Workers, Queues, and Cron Jobs for Stable Production Applications
Many web applications look simple from the outside: a user clicks a button, data is saved, and a status changes. But behind the scenes, stable production applications almost always run background work. Emails need to be sent, payments need to be checked, orders need to be monitored, caches need to be refreshed, files need to be processed, and notifications need to be delivered without slowing down user requests.
This is where workers, queues, and cron jobs matter. Without proper design, background processing can become an expensive source of bugs: jobs running twice, duplicated refunds, repeated emails, stuck statuses, or lost work after a server restart. This article explains practical background processing design for production web applications.
Worker, queue, and cron: what is the difference?
A cron job is a schedule. It runs a command at a fixed interval, such as every minute, every five minutes, or every night. A queue is a list of jobs waiting to be processed. A worker is the process that takes jobs from the queue and performs them.
For example, when a user registers, the application does not need to send an email inside the main request. It can enqueue a send-email job. A worker picks up that job and sends the email. If it fails, the worker can retry without making the user wait.
When is cron enough, and when do you need a queue?
Cron is enough for simple scheduled tasks: cleaning old sessions, checking pending payments, generating daily reports, or removing expired cache. Queues are better for event-driven work: sending email after registration, processing uploads, syncing order data, or sending per-user notifications.
If work can pile up and needs retry per item, use a queue. If work only needs periodic scanning, cron may be enough.
Classic background job problems
1. Jobs run twice
This can happen when cron overlaps, a worker restarts while a job is still running, or multiple servers run the same command. The solution is locking and idempotency. Locking prevents two processes from working on the same resource. Idempotency ensures that repeating a job is still safe.
2. Jobs fail silently
A dead worker without alerts can stop emails, orders, notifications, or synchronization. Important workers need health checks, error logs, and alerts when they stop processing work.
3. Retry repeats side effects
Retries are necessary, but dangerous when side effects are not safe. If a notification job retries three times, a user may receive three notifications. If a refund job retries without a guard, balance may be added more than once. Every data-changing job needs a unique guard.
Principles of safe worker design
- Every job has a unique identifier.
- A job can be retried without damaging data.
- Job status is stored: pending, processing, failed, done.
- Retry count and last error are recorded.
- Workers do not keep critical state only in memory.
- Important status or balance changes have audit logs.
Locking: simple but critical
Locks can live in a database, Redis, or file system, depending on architecture. For small applications, database locking is often enough. For example, when a worker takes an order, update its status from pending to processing only if it is still pending. If zero rows are affected, another worker already claimed it.
Do not select a row and assume you are the only process. In production, two workers can read the same row milliseconds apart. Use conditional updates or explicit locks.
Job idempotency
Idempotency means an action can be repeated but the final result remains the same. For example, a refund settlement job can use a key like REFUND-order123. Before adding balance, the system checks whether that key has already been used. If yes, the job is considered complete without adding balance again.
This matters for every job with side effects: refunds, charges, important emails, invoice generation, or terminal status updates.
Healthy retries
Do not retry every second forever. Use exponential backoff: retry after 10 seconds, 30 seconds, 2 minutes, 10 minutes, then mark as failed if it still fails. Store the last error so engineers can understand the cause. For permanent errors, such as invalid input, avoid excessive retries.
Cron overlap
A cron that runs every minute can overlap when one execution takes longer than a minute. Use a global lock per command. If the lock is still active, the next execution exits safely. Make sure the lock has a TTL so it does not remain forever when a process dies.
Minimum monitoring
- Number of pending jobs.
- Number of failed jobs.
- Age of the oldest unfinished job.
- Last successful worker activity.
- Average job duration.
- Retry count per job type.
Simple metrics are often more useful than complicated dashboards. If pending jobs keep growing, workers are too slow or failing. If the oldest job is too old, users will feel the delay.
Production checklist
- Stop a worker while a job is running, then start it again. Make sure the job is not lost.
- Run two workers at once. Confirm there is no double processing.
- Force an external API timeout. Confirm retry is safe.
- Test cron overlap. Confirm lock works.
- Restart during deployment. Confirm workers return to service.
- Read logs. Confirm errors are understandable.
A practical pattern for small applications
Small applications do not always need a complex queue system immediately. A database table as queue, a per-minute cron, and worker commands can be enough when designed well. The important part is not the most expensive technology, but the guarantee that jobs are not lost, not duplicated, and auditable.
Conclusion
Workers, queues, and cron jobs are foundations of stable production systems. They keep user requests fast, move heavy work into the background, and help systems recover from timeout or restart. But background jobs need locking, idempotency, retry strategy, and monitoring. Without them, background processing becomes a source of hard-to-debug bugs.
For digital products such as dashboards, payment systems, notifications, and API-based services, reliable background processing is not an optional feature. It is part of core reliability.