AgiCAD Home
Django Engineering April 27, 2026 · 10 min read

Django Background Jobs: When to Use Celery, Queues, and Scheduled Workers

Slow requests, unreliable webhooks, and long-running reports are usually signs that work has outgrown the request cycle. Here is how we design background processing for Django projects that need to stay maintainable.

A Django request should usually do one thing well: validate input, make the minimum necessary database changes, and return a response. The moment a view starts sending multiple emails, calling third-party APIs, resizing files, generating reports, or waiting on payment providers, the user experience and reliability profile change. A fast web request becomes a fragile workflow.

Background jobs solve that problem by moving slow or failure-prone work out of the request-response path. They are not only a performance tool. They are a boundary that lets a web app acknowledge user intent quickly while processing heavier work with retries, logging, and operational visibility.

What Belongs in a Background Job?

Good candidates share one or more traits: they take more than a few hundred milliseconds, depend on external systems, can be retried safely, or do not need to finish before the user sees a response.

  • Transactional and marketing email delivery.
  • PDF, CSV, image, or video generation.
  • Webhook processing from payment, CRM, or logistics providers.
  • Search indexing and cache warming.
  • Scheduled imports, exports, and synchronization jobs.
  • Long-running analytics reports.

The key phrase is "can be retried safely." A job system will eventually run a task twice: because a worker crashed after doing the work but before acknowledging the message, because a timeout was misconfigured, or because an operator manually retried a failed job. Design for that reality from the beginning.

When Celery Is Worth It

Celery remains the most common choice for serious Django background processing. It supports multiple brokers, scheduled tasks, retries, routing, result backends, and a mature ecosystem. It is a good fit when jobs are business-critical, queues need separate worker pools, or the application has enough volume that operational visibility matters.

Celery also adds operational cost. You need a broker such as Redis or RabbitMQ, worker processes, deployment scripts, monitoring, log aggregation, and clear ownership for failed jobs. For a small internal tool with one nightly cleanup task, Celery may be more machinery than the problem deserves. For a SaaS product sending thousands of emails and processing payment webhooks, it is usually justified.

Start with Job Boundaries, Not Tools

Before choosing infrastructure, describe the jobs in plain language. What triggers the job? What inputs does it need? What database rows does it create or update? What happens if it fails? Can it be retried? How will a support person know what happened?

A good background job receives durable identifiers, not large blobs of request data. Pass a user ID, order ID, document ID, or import ID, then load current state inside the task. This keeps messages small and avoids stale data when a task runs later than expected.

Good task input

send_invoice_email.delay(invoice_id=invoice.id)

Avoid passing full objects

send_invoice_email.delay(invoice.to_dict(), user.profile.to_dict())

Idempotency Is Non-Negotiable

Idempotency means running the same job twice does not create duplicate side effects. If a job charges a card, sends a contract, or creates a shipment, duplicate execution can become expensive immediately. Use unique constraints, status fields, external idempotency keys, and transaction boundaries to prevent duplication.

For example, a webhook processor should store the provider event ID and ignore events that have already been handled. An email job can record a sent timestamp or use an outbound message table. A report generation job can write to a known file path for the report version rather than creating unlimited duplicates.

Queue Design

Do not put every task into one queue forever. A slow video-processing job should not block password reset emails. A practical Django SaaS project often starts with three queues:

  • default for ordinary background work.
  • critical for user-facing jobs such as emails, payments, and webhooks.
  • bulk for imports, exports, reports, and heavy processing.

Separate worker pools let you scale and troubleshoot independently. If bulk reports are delayed, the support team should not lose password reset delivery at the same time.

Scheduling Recurring Work

Scheduled jobs deserve the same design discipline as user-triggered jobs. A nightly billing reconciliation should log what it checked, what it changed, and whether it found exceptions. It should be safe to rerun for the same date range. It should alert when it fails, not silently wait for a customer to notice.

Celery Beat works well for many teams, but the schedule itself becomes production configuration. Keep recurring jobs visible in documentation and operations dashboards. If a job matters to revenue or compliance, treat it as a first-class system component.

Observability and Failure Handling

A background job that fails quietly is worse than a slow request. At least a slow request tells the user something went wrong. Every important job should have structured logs, retry policy, failure alerts, and a way for operators to inspect state.

Retries should be deliberate. Network timeouts and temporary provider failures may deserve exponential backoff. Validation errors and missing database records usually should not retry forever. Separate transient failures from permanent failures so queues do not fill with work that can never succeed.

Summary

  • Move slow, external, and retryable work out of Django views.
  • Choose Celery when queueing is business-critical enough to justify operations.
  • Pass durable IDs into tasks, not large snapshots of request data.
  • Design every important job to be idempotent.
  • Use separate queues for critical, default, and bulk work.
  • Monitor scheduled jobs and failed jobs like production features.

Background processing is one of the points where a Django project starts behaving like a real product system rather than a collection of views. The tool choice matters, but the discipline matters more: clear job boundaries, safe retries, observable failures, and queues that protect the user-facing experience.

If your Django app has outgrown synchronous workflows, talk to AgiCAD. I help teams design background processing, API workflows, and operational foundations that can support real production load.