Django Celery Execute only one instance of a long-running process
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
If you need only one instance of a long-running Celery task to run at a time, the important requirement is not "only one worker thread touches this code." The real requirement is a distributed lock that works across all workers and all machines.
A process-local flag or a Python global variable is not enough. Celery workers are separate processes, and in real deployments they may run on several hosts.
Decide What "Only One Instance" Means
There are two common meanings:
- only one task of this kind may run globally
- only one task with the same argument set may run at a time
That distinction affects the lock key. A task that imports one customer account at a time may use a per-customer lock, while a nightly global sync may use a single global key.
A Simple Distributed Lock With Redis
One practical pattern is to use Redis and take a lock before doing the work:
cache.add(...) is useful because it succeeds only if the key does not already exist. That gives you a basic mutual-exclusion mechanism across workers, assuming the cache backend supports atomic add behavior.
Configure a Backend That Actually Supports This
This pattern only works if the backend is shared and atomic. A local-memory cache is not sufficient. A Redis-backed cache is a common choice:
The shared backend is what makes the lock visible to every worker process.
Lock Expiry Is Essential
Always set a timeout on the lock. If a worker crashes after acquiring it, a lock with no expiry can block the task forever.
The timeout should be:
- long enough for normal completion
- short enough that a stale lock eventually clears
If the task duration varies a lot, you may need a more advanced heartbeat or renewable-lock strategy.
Argument-Specific Singleton Behavior
If you want only one task per key or per argument set, incorporate the arguments into the lock:
Now two different customers can sync concurrently, but the same customer cannot be processed twice at once.
Third-Party Helpers Can Reduce Boilerplate
Packages such as task-singleton helpers can wrap this pattern for you, but they are still built on the same underlying idea: a shared lock outside the worker process.
That can be worth it if your project uses many singleton tasks. If you only need one or two, an explicit lock often stays easier to reason about.
Prevent Queue Buildup If Necessary
There is a second problem beyond simultaneous execution: duplicate task scheduling. Even if the lock blocks duplicates from running, repeated enqueueing can still pile up many skipped tasks.
If that matters, consider deduplicating before enqueue, or use a task pattern that checks whether equivalent work is already scheduled or active.
Common Pitfalls
The biggest pitfall is using an in-memory Python variable as the lock. That only protects one process, not the Celery fleet.
Another common issue is forgetting lock expiry. If a worker dies, the job can become permanently blocked behind a stale lock.
People also use one global lock when they really need a per-argument lock, which accidentally serializes unrelated work.
Finally, skipping duplicate execution is not the same as preventing duplicate queue entries. Decide whether you need one or both protections.
Summary
- To run only one Celery task instance at a time, use a real distributed lock.
- Redis-backed cache with atomic
addis a practical building block. - Put the lock release in
finallyand always use an expiry timeout. - Include task arguments in the lock key if singleton behavior should be per resource rather than global.
- Consider deduplication at enqueue time if duplicate queued tasks are also a problem.

