How Plane Uses Celery Tasks to Handle Asynchronous Operations: Notifications, Exports, and Email
Plane offloads all time-consuming I/O operations to Celery workers, using a beat scheduler for periodic jobs and Redis locks to prevent duplicate email sends.
Plane, the open-source project management platform, leverages Celery to handle asynchronous operations like notifications, exports, and email sending outside the Django request/response cycle. By queuing tasks in the plane.bgtasks package and processing them via dedicated workers, the application maintains responsive API endpoints while background jobs handle data serialization, file uploads, and message delivery.
Celery Configuration and Task Discovery
The asynchronous architecture centers on plane.celery, which instantiates a Celery app and loads configuration from plane.settings.common. The setup uses app.autodiscover_tasks() to automatically locate every task decorated with @shared_task within the plane.bgtasks package. This modular approach ensures that any new background task placed in the bgtasks directory is immediately available to workers without manual registration.
The beat scheduler—powered by django-celery-beat—defines periodic jobs in the beat_schedule configuration. Key recurring tasks include a five-minute email queue flush and daily maintenance routines, ensuring timely delivery and system cleanup without blocking user requests.
Handling Notifications Asynchronously
When issue-related events occur—such as comments created or state changes—the API view enqueues the notifications task using notifications.delay(...). This task, defined in apps/api/plane/bgtasks/notification_task.py, performs two distinct operations:
- In-app notifications: Builds
Notificationobjects for every subscriber who should receive an in-app badge. - Email logging: Creates
EmailNotificationLogrows for users whose preferences allow email alerts.
These log rows are not sent immediately. Instead, they persist in the database as a queue, decoupling the notification creation from the actual email transmission to allow for batch processing and retry logic.
Processing Email Queues with Periodic Tasks
Email delivery runs on a separate schedule managed by the beat job check-every-five-minutes-to-send-email-notifications. Every five minutes, this job executes plane.bgtasks.email_notification_task.stack_email_notification, which groups pending EmailNotificationLog entries by receiver.
For each receiver batch, the system fires a send_email_notification task via send_email_notification.delay(...). This task implements several safety mechanisms:
- Redis locking: Obtains a lock using
acquire_lockbefore processing to prevent duplicate sends when schedules overlap. - Template rendering: Uses Django’s
EmailMultiAlternativesto construct and deliver the message. - Status updating: Marks log rows as sent after successful delivery, then releases the lock via
release_lock.
This pattern ensures exactly-once delivery semantics even when multiple workers process the same queue.
Background Export Generation
Export operations follow a similar asynchronous pattern. When a user triggers an export from the UI, the view immediately calls issue_export_task.delay() with parameters including provider type, workspace ID, project IDs, and a token ID.
The task in apps/api/plane/bgtasks/export_task.py executes the following workflow:
- Queries the required issues and serializes them through
DataExporter(supporting CSV, JSON, or XLSX formats). - Writes each file into a ZIP archive using
create_zip_file. - Uploads the archive to S3 or MinIO via
upload_to_s3. - Updates the
ExporterHistoryrecord with a pre-signed URL for download.
Because the export task runs completely off-thread, the HTTP request returns immediately with a token that the frontend polls to track completion status.
Safety Mechanisms and Task Orchestration
Plane implements several safeguards to ensure reliable background processing:
Redis Locks for Idempotency: Long-running or idempotent jobs like email sending use Redis-based locking (acquire_lock/release_lock) to prevent duplicate execution when beat schedules overlap or workers retry failed tasks.
Periodic Maintenance: The beat_schedule in apps/api/plane/celery.py defines additional periodic tasks for hard deletes, cleanup operations, and metric pushing, ensuring the system remains performant without manual intervention.
Shared Task Decorator: All tasks use the @shared_task decorator, making them discoverable by the autodiscovery mechanism and ensuring compatibility with the Celery app configuration.
Summary
- Plane uses Celery workers and a beat scheduler to handle asynchronous operations outside the Django request cycle.
- Notifications are created immediately via
notifications.delay()but emails are queued inEmailNotificationLogrows for batch processing. - The
check-every-five-minutes-to-send-email-notificationsbeat job aggregates and sends emails using Redis locks to prevent duplicates. - Exports run via
issue_export_task, which serializes data, creates ZIP archives, uploads to S3, and updatesExporterHistoryrecords asynchronously. - All tasks reside in
plane.bgtasks, use@shared_task, and are auto-discovered by the Celery app configuration.
Frequently Asked Questions
How does Plane prevent duplicate email notifications?
Plane uses Redis-based locking mechanisms within the send_email_notification task. Before processing a batch, the task calls acquire_lock to obtain a lock, processes the emails, and then calls release_lock. This prevents multiple workers from sending the same notification when the five-minute beat schedule overlaps or during task retries.
What triggers the export task in Plane?
When a user initiates an export from the UI, the API view calls issue_export_task.delay() with parameters including the provider type (CSV, JSON, or XLSX), workspace ID, project IDs, and a unique token ID. This immediately queues the background task, allowing the HTTP response to return while the worker handles data serialization and file upload.
How often does Plane process queued emails?
Plane processes the email queue every five minutes via the check-every-five-minutes-to-send-email-notifications beat schedule. This periodic task runs stack_email_notification, which aggregates pending EmailNotificationLog entries and dispatches individual send_email_notification tasks for each receiver batch.
What is the purpose of the plane.bgtasks package?
The plane.bgtasks package serves as the dedicated location for all Celery background tasks, including notification handling, email delivery, and export generation. Using app.autodiscover_tasks() in plane.celery, the system automatically discovers any task decorated with @shared_task within this package, ensuring modular and scalable task registration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →