Cron Jobs, Background Workers and Queues: Choosing the Right One
Three primitives that answer three different questions. What each one does when it fails, why the syntax half of this topic is disappearing from search, and where the job actually runs.
The page Google still ranks first for this comparison is a thread titled "Do I need a worker, cron job or both?" It was posted to r/webdev eleven years ago and it has three comments.
Eleven years and three comments is not a knock on the thread. The top answer is good: workers are for things that happen in response to an event, crons are for things that happen at a time. It is a comment on the question, which has been asked continuously since 2015 and has never been given a page that settles it. Below the thread sit a small blog, Railway's documentation, a Stack Overflow question, an article from Render, a Nextcloud support thread and a LinkedIn post.
This is the pillar for how recurring and deferred work gets run. It covers the three primitives, the questions each one answers, what each does when it goes wrong, and one thing none of the pages above raise: where the job physically executes and who is responsible for the data it touches while it runs.
One word of warning about searching for this
"Background jobs" is a homonym, and in Google's German results it is mostly not about software: the query returns an employment widget, and the surrounding searches are for work as a film extra (background actor jobs, background dancer jobs) and for pre-employment background checks. The rest is SAP administration. If your searches are coming back strange, that is why. Pair the phrase with worker, queue or cron and the results change completely.
Three primitives, three different questions
Render puts the distinction well enough to quote rather than paraphrase: "Cron jobs answer when work runs. Background workers answer how much work you can absorb." That is two of the three. The queue is the third, and it answers what happens to work you cannot absorb yet.
| Primitive | The question it answers | What starts it | When it ends |
|---|---|---|---|
| Cron job | *When* does this run? | A clock | When the command exits. It is expected to exit. |
| Background worker | *How much* concurrent work can we absorb? | Process start, once, at deploy | Never, by design. Restarting it is an event, not a routine. |
| Queue | *What happens* to work we cannot do right now? | Your application, on an event | Not a process at all. It is the buffer the worker reads from. |
Railway's documentation makes the worker half concrete: "A background worker is an always-on service that processes work continuously. Unlike a cron job, it does not exit after completing a task." The word doing the work in that sentence is *exit*. A cron job that does not exit is a bug, because the next scheduled run is coming. A worker that exits is an outage, because nothing is going to start it again until something notices.
That gives you a usable first cut. Nightly invoice generation is a cron job: it has a time, it finishes, and nobody is waiting. Resizing an uploaded image is a queue and a worker: it has no natural time, someone is waiting, and the arrival rate is set by your users rather than by you. Sending a password reset email is neither, most of the time; it is a request that should just be handled, and pushing it into a queue buys reliability at the cost of latency the user can feel.
What each one does when it fails
The comparison usually stops at the table above. The table is the easy half, and it is not where the decision actually goes wrong. Each primitive has a characteristic failure, and picking the wrong one means signing up for a failure mode you have not thought about.
| Cron job | Worker | Queue | |
|---|---|---|---|
| What breaks first | The run that never happened | The process that quietly died | The backlog nobody is draining |
| How you find out | Something downstream is stale, usually days later | Latency climbs, then work stops arriving anywhere | Depth metric, if you have one. Otherwise a customer. |
| Does it retry? | No. Classic cron does not catch up on missed runs. | Only if you wrote the retry | Usually yes, and that is its main advantage |
| Failure you did not expect | Two runs overlapping when one is slow | One instance doing work twice after a restart | A poison message retried forever |
The run that never happened
A cron job that does not fire produces no error, no log line and no alert. There is nothing to fail. If the machine was asleep, restarting, or in the middle of a deploy at 03:00, the 03:00 run does not happen and classic cron does not go back for it. This is the reason anacron exists on desktop and laptop systems, and it is worth knowing the distinction as of writing: anacron works in periods rather than at times and will run a job late if it was missed, while cron will not.
Deploys are the case that catches teams out, because a deploy is exactly when the machine is least likely to be able to run anything, and deploys cluster in working hours while nightly jobs do not. Rolling releases behind a health check narrow the window without closing it. The practical defence is that the job should notice: a task that records when it last completed, and an alert on that timestamp being older than it should be, catches the silent case that nothing else will.
Background Job Workers instead of Cron?
That is a Nextcloud support thread from January 2026, quoted as titled. The user writes: "I've been using systemd timer, and crontab before that, but after updating to the latest version a week ago background jobs weren't running." It is the whole category in one sentence. The schedule was fine. The thing the schedule was supposed to start had stopped working, and the schedule had no opinion about that.
The second failure in this family is overlap. A job scheduled every five minutes that starts taking six will eventually be running twice, and if it was not written for that, the two copies will fight over the same rows. What should happen next is a real decision with three sane answers: skip the new run, queue it, or kill the old one. That decision has its own name and its own article coming, so it is enough here to say that if a scheduled job can ever run long, you have already made the decision by default and the default is "run both".
systemd timer is worth naming as the honest alternative on a single machine. It handles missed runs, it logs to the journal, and it can express dependencies that a crontab line cannot. It is also one more thing to administer on a box you maintain, which is the trade the next section is about.
Is cron outdated?
Google asks this question itself. "Is cron outdated?" appears in the People Also Ask block on both the comparison search and the plain cron job search, sitting next to "What replaced cron jobs?". Two versions of the same suspicion, surfaced by Google rather than by a vendor.
The measurable answer is more interesting than yes or no. Search demand for this vocabulary in Germany is falling across the board year on year as of writing, and the steepest falls are all in one half of it: python cron job is down 71%, cron job raspberry pi 64%, cron job every 5 minutes 56%, cron job logs 56%, cron job ubuntu 46%, cron job expression 32%, cron job syntax 18%. Not one phrase in the set is growing.
Look at what is falling and the pattern is not that people stopped scheduling things. It is that they stopped searching for how to write the schedule. The reason is visible in the results themselves: Google's generated answer for cron job now prints the five-field table and this line before any link at all.
* * * * * /path/to/script.sh
# minute hour day-of-month month day-of-weekSo the honest answer to "is cron outdated" is that the primitive is fine and the craft around it has been absorbed. Scheduling a job at a time remains the correct shape for a large class of work, and no queue replaces it. What has aged is hand-editing a crontab on a machine you personally administer, along with the assumption that knowing the syntax is the skilled part of the job. The syntax is now the part a search engine hands you for free; what it does not hand you is the failure table above.
What replaced cron jobs?
Nothing replaced the primitive. What changed is where it runs. On a single server the successor to a crontab line is a systemd timer, and in a cluster it is a Kubernetes CronJob, which as of writing does the same thing with a controller behind it. On a managed platform it is a scheduled job defined next to the application, which is the version that removes the machine from the picture entirely.
Google is confused about the words too
One of the four questions in that People Also Ask block reads, exactly as written: "What is the difference between a cron job and a cron job?" It is not a typo on this page. The term genuinely carries three meanings that people use interchangeably — the daemon, the crontab entry, and the scheduled unit of work a platform runs for you — and the question, broken as it is, is a fair summary of the state of the vocabulary.
Where the job actually runs, and who is the processor for it
None of the pages ranking for this comparison ask where the work executes. It is a strange omission, because a background job is usually the part of a system that touches the most data at once.
Consider what a routine nightly job does. It connects to the production database and reads rows about real people. It writes a log line for each one. It renders and sends email, so it hands names and addresses to a third party. It may write a report to object storage that nobody has classified. A request handler touches one user's data; a batch job touches everyone's, and it does so unattended, at an hour when nobody is watching.
That makes the execution location a question with legal weight rather than an operational detail. Under the GDPR you remain the controller for all of it, and whoever runs the machine the job executes on is a processor for that layer. Which layers come with a processor attached and which you carry alone depends on the service model you bought, and it is the same line for a worker as for the web application. The email relay, the error tracker and the storage bucket the job wrote to are sub-processors you inherited, and what your data processing agreement has to cover includes all of them.
Two practical points fall out of that. Jobs and application should run in the same jurisdiction, because a scheduler in one region reaching into a database in another is a transfer whether or not anyone described it as one. And job logs deserve the same retention thinking as any other store of personal data, which they rarely get, because a log line written at 03:00 by a script nobody reads is the easiest place in a system to accumulate records indefinitely.
Choosing: four questions, not a feature table
- Does the work have a time, or a trigger? A time means a schedule. A trigger means a queue and something reading it. If the answer is "a trigger, but we check for it on a schedule", you have a queue implemented as a cron job polling a table, which works and is worth naming so you know what you built.
- Is anyone waiting for it? Nobody waiting means a schedule is fine and running late costs nothing. Someone waiting means latency is a feature, and the arrival rate is set by your users rather than by you, which is the definition of the case a worker exists for.
- What happens if it runs twice? If the answer is "nothing", most of the difficulty in this article disappears. If the answer is "we double-charge someone", then idempotency is the actual requirement and the choice of primitive is secondary to it.
- Does it call an API with a quota? If the work depends on an upstream that can refuse you, the queue stops being optional. Handling a 429 and the Retry-After header is a retry problem, and a retry problem wants somewhere durable to retry from.
- What happens if it never runs? If the answer is "we find out from a customer", the missing piece is not a primitive at all. It is a completion timestamp and an alert on it, and that applies equally to all three options.
Most production systems end up with all three, which is the outcome the r/webdev thread arrived at eleven years ago. A schedule for the work that has a time, a queue for the work that has a trigger, and a worker draining the queue. The reason to be deliberate about which is which is that they fail differently, and a system where all three are the same thing fails in whichever way you were least prepared for.
How Runsite handles it
Both shapes exist on the platform rather than only one. Scheduled jobs run as cron jobs on Runsite against the same repository and the same environment as your application, so the schedule is deployed with the code instead of living in a crontab on a machine somebody has to remember. The always-on shape is a web service that happens not to serve HTTP: a worker process started at deploy and restarted with the application.
The queue is the piece we do not invent. Managed Redis is where most teams put one, and it is worth being clear that using Redis as a queue rather than as a cache changes what happens when it fills up, because a queue is durable data and an eviction policy will drop it without an error. The job's data lives in managed PostgreSQL and whatever it writes goes to object storage on the same invoice.
On the jurisdiction question from the previous section: servers are in Germany as of writing, so the scheduler, the worker, the database and the storage are in the same place, and a signed GDPR data processing agreement covers every plan including the free one. That is the specific thing a crontab on your own box cannot give you, because there the obligation for the layer the job runs on has nowhere to sit except with you. A worked example of a real scheduled task, written out end to end, is the automated PostgreSQL backup to S3. Setup details are in the Runsite docs.
The short version
- Three primitives answer three questions. Cron answers when work runs, a worker answers how much concurrent work you can absorb, and a queue answers what happens to work you cannot absorb yet.
- A queue is not a third kind of process. It is the state between the thing producing work and the thing consuming it, which is why workers and queues are usually bought together.
- The word that separates cron from a worker is *exit*. A cron job that does not exit is a bug; a worker that exits is an outage.
- Choose on the failure mode, not the feature table. A missed cron run produces no error at all, a dead worker shows up as latency, and a queue nobody drains shows up as a customer complaint.
- Classic cron does not catch up. A run that fell inside a reboot or a deploy window is simply gone, which is what
anacronwas built to address and what a completion timestamp plus an alert actually solves. - If a scheduled job can ever run long, you have already chosen an overlap policy and the default is "run both".
- "Is cron outdated?" is Google's own question, in the People Also Ask block on both searches. The primitive is not outdated; hand-editing a crontab on a machine you administer is.
- The demand data says the same thing. Every measurable phrase in this vocabulary is falling year on year in Germany as of writing, and the steepest falls are all syntax queries, because Google's generated answer now prints the five fields before any link.
- Nothing replaced cron.
systemd timeron one machine,CronJobin a cluster, a scheduled job next to the application on a platform. The primitive moved, it did not retire. - A batch job touches everyone's data at once, unattended. Where it executes is a question about jurisdiction and sub-processors, not a deployment detail, and no page ranking for this comparison raises it.
- Ask four questions: does the work have a time or a trigger, is anyone waiting, what happens if it runs twice, and what happens if it never runs. The last one is answered the same way whichever primitive you pick.