Building Reliable Automations With Queues and Background Workers
A practical guide to deciding what belongs in a request, a durable job queue, or a Pub/Sub event, and the reliability details that prevent retries from becoming duplicate side effects.
Updated October 22, 2026
The request that creates an order should not also render a PDF, email it, resize every image, update an analytics dashboard, and wait for three third party APIs. It should persist the order safely, return a useful response, and hand slow work to a system that can retry it.
Queues and background workers let those operations continue with their own status and retry policy. The application still needs to record what was requested and make the eventual outcome visible.
First decide whether the user must wait
| Work | Usually synchronous | Usually asynchronous |
|---|---|---|
| Validate a payment request | Yes, return a clear result | Sometimes, if the provider has a delayed confirmation flow |
| Create an order | Yes, persist the durable record | Follow up fulfilment work |
| Generate a large PDF | No, return the order state | Yes, worker creates and stores the document |
| Send email or webhooks | No | Yes, retry outside the user request |
| Refresh a search or RAG index | No | Yes, batch and observe it |
The distinction is about the user facing contract, not about whether a task feels important. A background job can be business critical; it simply needs a durable state and a recovery path.
Queue and Pub/Sub are not interchangeable
A queue distributes work among consumers, with acknowledgement and redelivery behaviour determined by the broker. Publish/subscribe distributes events to subscriptions. Some pub/sub systems retain events for durable subscriptions; others deliver only to currently connected subscribers.
Redis Pub/Sub is good for transient fan out such as real time UI updates. It has at most once delivery: an offline or failing subscriber can miss a message. For work that must survive a restart or be retried, use a durable queue or stream with acknowledgements, such as a Redis Streams based worker system, SQS, RabbitMQ, or another queue that matches the operational requirements.
The outbox closes the gap between committing a database record and enqueueing its work. A crash between those two operations can otherwise leave an order saved with no PDF job. Delivery may still be repeated, so consumers must remain safe to retry.
Make the worker safe to retry
Retries are expected. Networks fail, provider APIs time out, and deployments interrupt running work. The worker must be designed so that retrying does not create a second charge, email, or document.
Idempotency key
Give every side effect a stable identifier and enforce deduplication at the side effect boundary, using provider idempotency or an appropriate database rule. Logging an ID alone cannot prevent duplicates after a crash.
Dead letter queue
After a bounded number of attempts, park the failing job with its error and payload. Do not endlessly retry poison messages or silently drop them.
Other useful rules: set timeouts for every external call, use backoff with jitter, make the worker observable, and separate “retry later” from “a person must review this.”
A practical stack is still a product choice
For an early stage service, a containerised application on a Hetzner server, deployed through Coolify, with PostgreSQL and a Redis backed durable worker can be enough. For a team using AWS, SQS and managed compute may reduce operational burden. The same design principles apply either way:
- Persist intent before enqueueing work.
- Track job state explicitly:
queued,running,completed,failed, andcancelledare much clearer than a spinner. - Monitor queue age, failure rate, retries, and worker heartbeats.
- Keep logs and metrics together so a failed job can be traced from request to worker.
The check I use before adding asynchronous work
- Does the user need the completed result before the request can return?
- If the process restarts, can this work be recovered without guessing?
- If it runs twice, can it create a duplicate side effect?
- Can an operator see why a job failed and decide what to do next?
If the answer to those questions is clear, a queue becomes a reliability tool, not a source of invisible complexity.
For the precise transport guarantees, see Redis Pub/Sub and Redis Streams.