Write the cron every minutes line first: five asterisks run a job every minute, and */5 in the first field runs it every 5 minutes. Then answer the two questions the line cannot. Which machine actually runs it, and how will you know the night it silently stops? The syntax is the easy part.

Cron every minutes: the lines for every 1, 5, 10, 15 and 30

A cron line has 5 fields: minute, hour, day of month, month and day of week. Five asterisks run a job every minute. A step in the first field sets the interval: */5 for every 5 minutes, */10, */15 or */30 for the others. Standard cron has no seconds field.

Cron is the daemon and the crontab is the file it reads, so a cron line and a crontab line for every few minutes are the same thing. Scheduled work is one part of hardening SaaS applications for resilience. The times in the line are clock times in one zone, and anything the job does with dates should follow the time zone handling checklist.

Each crontab job below, from every minute to nightly, is one line to paste after crontab -e, followed by the command it runs.

ScheduleThe line
Every minute* * * * *
Every 5 minutes*/5 * * * *
Every 10 minutes*/10 * * * *
Every 15 minutes*/15 * * * *
Every 30 minutes*/30 * * * *
Every hour, on the hour0 * * * *
Every night at 03:000 3 * * *

The five fields come in this order, with the values the crontab(5) manual allows.

FieldAllowed values
minute0-59
hour0-23
day of month1-31
month1-12, or names
day of week0-7, where 0 and 7 are Sunday, or names

A step walks through the field’s range from its first value, and it is evaluated only within that field. That is why 5, 10, 15 and 30 give an even rhythm: each divides 60. The manual’s own warning is */35, which runs at 0 and 35 minutes past each hour, not every 35 minutes.

Cron checks its table once a minute, so no line gives you every 10 seconds. For that you need a scheduler that accepts seconds, such as Supabase Cron on Postgres 15.1.1.61 or later, or a loop inside a process that is always running.

crontab -e opens your crontab in your editor and installs it when you exit, and crontab -l prints it. Each user can have their own, and its commands run as the user who owns it.

Five traps come with the line:

  • Environment: cron sets SHELL to /bin/sh, takes HOME and LOGNAME from the user’s account, and sets its own PATH unless the daemon runs with -P. Your app’s env file is nowhere in that, so use absolute paths and load the env file in a wrapper script.
  • Time zone: cronie, the cron that manual documents, accepts a CRON_TZ line for the whole table. Times inside a daylight saving gap never run, and a repeated hour runs its jobs twice.
  • Overlap: a job that can outlast its interval needs a lock. With -n, flock(1) fails at once instead of waiting when the lock is held, with exit status 1 by default.
  • Output: cron mails whatever the job prints to the crontab’s owner, or to MAILTO if it is set, and falls back to syslog when sendmail is not installed. Send stdout and stderr to a log file you actually read.
  • Location: a crontab lives on one machine, under /var/spool/cron/, and it is not in your repository unless you keep a copy there and install it from the repo on deploy.

Here is a five-minute job with the lock and the log redirect in place:

# crontab -e, as the user the app runs as
# CRON_TZ is a cronie setting for the whole table
CRON_TZ=UTC
*/5 * * * * /usr/bin/flock -n /tmp/reconcile.lock /bin/sh $HOME/app/bin/reconcile.sh >> $HOME/logs/reconcile.log 2>&1

The wrapper loads the environment cron does not give it:

#!/bin/sh
# bin/reconcile.sh: assumes .env holds plain KEY=value lines
set -a; . "$HOME/app/.env"; set +a
# absolute path to node: check yours with `command -v node`
cd "$HOME/app" && exec /usr/bin/node scripts/reconcile.js

On a Linux host that uses systemd, a timer unit does the same job: OnCalendar=*:0/5 fires every 5 minutes, Persistent=true triggers the job at once if a run fell due while the timer was inactive, such as while the machine was powered down, and systemctl list-timers shows when each timer last ran.

Why it matters: a scheduled job fails without telling anyone

A web request that fails shows an error to someone, even if that someone is only the customer. A scheduled job that stops shows nothing to anyone: there is no page to load and no user waiting on the answer. That is my reading, not a measured fact.

The jobs below are illustrations of what a small SaaS tends to run, not incidents.

The jobWhat quietly breaks when it stopsWho notices first
Billing reconciliationDrift between your database and the payment provider grows unseenA customer billed wrongly, or whoever closes the month
Trial and renewal emailsTrials end without a warning and renewals arrive unannouncedCustomers
Nightly backup dumpNothing visible, until the day you need a restoreYou, on that day
Cleanup of expired sessions, uploads or soft-deleted rowsStorage and the database keep growingThe disk, or the bill
Weekly reportThe report stops arrivingNobody

Drift in the first row is the costly one, and keeping Stripe subscriptions in sync with the database is a scheduled job for exactly that reason.

Jobs stop for dull reasons: a deploy moved the app to a host with no cron, the server was rebuilt and the crontab was not, the token the job used expired, or the job now takes longer than its interval. A free database can go quiet too. Supabase pauses Free Plan projects that show low activity over a 7-day period, and its pausing page says nothing about what happens to scheduled jobs.

Whether a failed run leaves any trace depends on error tracking. In my June and July 2026 audits, 17 of the 21 third-party apps had no error tracking or alerting: when a user hits an error, nothing records it. Those 21 are the apps I audited, a selected set, not a random sample and not a rate for AI-built apps in general.

I audited an AI coding workspace that had no error tracking in its code (the report notes the owner might run a log drain the code does not show); an observability package was a declared dependency imported nowhere, and every API route’s error handling was a bare console line in the host’s short-lived function logs, the opposite of application monitoring best practices for a small SaaS. The lesson I take from it: an error written only to a short-lived log is an error nobody reads, and a scheduled job has no user to complain on its behalf.

How it works: where the job actually runs, and how you know it ran

The line says when. The five parts below say where it runs (on Windows, on the common hosts, on AWS, inside the app) and how to prove each run happened.

Cron on Windows: there is no crontab, and the schedule belongs on the server

Cron on Windows is not part of the system, because cron is a scheduler for Unix-like operating systems. The Windows equivalent is Task Scheduler, driven from the command line by schtasks. For a web app that matters less than it seems: the production schedule belongs on the server or the hosting platform, kept under version control.

Whether you want a cron job or a crontab entry, Windows answers with the same tool. Microsoft’s schtasks /create reference gives the minute schedule: /sc minute, with /mo setting the minutes between runs, from 1 to 1439.

schtasks /create /sc minute /mo 5 /tn "Reconcile" /tr "C:\app\bin\reconcile.cmd"

Microsoft’s own example adds the catch for a laptop: the task runs every N minutes whenever the system is running.

You wantOn Windows use
A recurring task on a Windows serverTask Scheduler, created with schtasks or its window
Cron syntax on a Windows laptop, for developmentA Linux distro in WSL, which shuts down after it has been idle for 15,000 milliseconds by default, so its jobs run only while it is up
The production job for a web appNot your laptop at all: the server or platform that runs the app

The last row is the one that matters. If the app runs on Linux, in a container or on a serverless host, the schedule belongs there, in the repository, and the developer’s operating system stops mattering. Third-party cron ports for Windows exist; I would not put a billing job on an unmaintained script.

Where a scheduled job runs on the hosts a small SaaS uses

A scheduled job needs a scheduler that stays alive, and on a serverless host that scheduler is never your app’s own process. A small SaaS has 5 usual places: a server’s crontab, the hosting platform’s cron feature, the database’s scheduler, a CI schedule, or a cloud scheduler. Each has its own time zone and failure behavior.

For which hosts offer background work at all, start with the deploy target comparison. The table below covers how a job is declared on each and what happens when a run goes wrong, from each provider’s docs as read on 2026-09-30.

HostHow the job is declaredTime zoneShortest intervalA failed or overlapping run
VPS or VMA crontab line, or a systemd timerThe server’s, unless CRON_TZ sets oneOne minute for cron; systemd timers accept seconds once AccuracySec= is lowered from its 1-minute defaultRetries not stated in the cron manual; runs overlap unless you lock; output is mailed
Vercelcrons in vercel.json, each calling a route with an HTTP GETAlways UTCOnce per minute on Pro and Enterprise, once per day on HobbyNo retry on failure; a long run can overlap the next; delivery is best effort and can occasionally invoke the same run more than once
SupabaseSupabase Cron on pg_cron, running SQL, a database function or an HTTP requestGMT in the docs’ examplesEvery second, on Postgres 15.1.1.61 or laterEach run and its status go to cron.job_run_details; retries not stated in Supabase’s docs
RenderA cron job service with a schedule and a commandUTCNot stated in Render’s docsAt most one run is active; the next run waits for it; a run is stopped after 12 hours
NetlifyA scheduled function, with schedule in its config or netlify.tomlUTCNot stated in Netlify’s docs30-second execution limit; retries not stated in Netlify’s docs
GitHub Actionson: schedule in a workflow fileUTC unless you give an IANA zoneOnce every 5 minutesRuns can be delayed at busy times, and under high enough load some queued jobs may be dropped

The sources are Vercel’s cron jobs docs, Supabase Cron, Render’s cron jobs docs, GitHub’s schedule event docs and Netlify’s functions docs. A few lines in them change how you build the job. Vercel will not retry an invocation if a cron job fails, and its Hobby plan restricts users to non-commercial, personal use only. GitHub disables scheduled workflows in a public repository after 60 days without activity, which, with the runs that may be dropped, makes it fine for a nightly chore and wrong for billing, in my view. Netlify runs scheduled functions on their schedule only for published deploys.

The trap on serverless hosts is a timer inside the app. A setInterval or an in-process scheduler does nothing while no process is alive, and on a serverless host you do not decide when a process is alive, so the schedule has to be the platform’s.

A platform cron job on Vercel is an ordinary route, and anyone who finds its URL can call it. Vercel sends the value of a CRON_SECRET environment variable as an Authorization header when it invokes the job, and the route should reject any request that lacks it. Netlify’s scheduled functions cannot be invoked directly with a URL at all.

A job that outlasts the host’s function limit should only enqueue the work and let a worker finish it, the pattern behind how to run long tasks in the background.

AWS scheduled task: EventBridge Scheduler, rules and ECS

An AWS scheduled task is usually one of three things: an EventBridge Scheduler schedule that invokes a target such as a Lambda function, an older EventBridge rule with a schedule expression, or an ECS scheduled task that runs a container. AWS cron expressions are not identical to crontab lines.

Each scheduled task that AWS runs through EventBridge Scheduler needs an execution role that lets the scheduler call its target, so permissions are the first thing to check when one does nothing.

AWS optionWhat it triggersWhen to use it
Amazon EventBridge SchedulerLambda, SQS, SNS, EventBridge, or the APIs of more than 270 AWS services, on a cron, rate or one-time scheduleNew schedules; AWS recommends it
EventBridge scheduled ruleThe rule’s targets, on a rate or cron expression in UTC+0Existing setups only; AWS calls scheduled rules a legacy feature
ECS scheduled tasksOne or more tasks in your ECS cluster, on a schedule created in EventBridge SchedulerA job that needs a container, or one that runs past Lambda’s 900-second function timeout

Do not paste a crontab line into AWS. A Scheduler cron expression has six required fields, the last being the year, written cron(minutes hours day-of-month month day-of-week year), and you cannot use * in both day fields: one of them takes ?. Every 5 minutes becomes cron(0/5 * * * ? *), or more simply rate(5 minutes). Scheduler evaluates cron and one-time schedules in UTC or in a time zone you set, while scheduled rules always use UTC+0.

When a target fails, Scheduler retries up to the limit you set, and with a dead-letter queue configured, the failed invocation lands in an SQS standard queue once those retries are exhausted. In CloudWatch, TargetErrorCount counts target failures and InvocationDroppedCount counts runs dropped after the retry policy ran out; an alarm on either is how AWS tells you a run failed, bearing in mind that AWS delivers these metrics on a best-effort basis. AWS’s pricing page lists 14,000,000 free Scheduler invocations a month under its Free Tier (checked 2026-09-30).

Agenda npm and node-cron: schedulers inside the app process

Agenda is a Node.js scheduler from npm that stores jobs in a database and locks each run so only one worker takes it. Lighter libraries such as node-cron keep timers in memory. A plain in-memory timer is safe only with exactly one always-on process, and each process fires the job once there are two.

The agenda package on npm is at version 6.2.6, released on 2026-07-21, and Agenda on GitHub is not archived. It is ESM-only, needs Node.js 18 or later, and stores jobs through a backend package for MongoDB, PostgreSQL or Redis. node-cron runs cron expressions down to the second inside your process and does not persist state to a database; its distributed: true option ensures only one instance executes each scheduled fire, using an env-var flag out of the box or a Redis coordinator for high availability.

LibraryWhere the schedule livesSurvives a restartSafe with two app instances
AgendaIn a database, through its MongoDB, PostgreSQL or Redis backendYes: jobs are storedYes: its lock stops two processors running the same job
node-cronIn the process’s memory, defined in your codeThe schedule returns with the code; its state is not persisted to a databaseOnly with distributed: true set, coordinated by an env-var flag or a Redis coordinator
A bare setIntervalIn the process’s memoryNo, it starts again from zeroNo: each instance fires (my reading)

My rule: keep an uncoordinated in-memory scheduler to apps with exactly one always-on process, because the day you scale to two, the job runs twice. A persisted, locking scheduler such as Agenda, or a queue with repeatable jobs, is built for many workers. None of them runs on a serverless host, for the reason in the host section. Whichever one runs the job, the job itself must be safe to run twice, which is what an idempotency key for safe retries gives you.

How you know it ran: the log line, the exit code, the heartbeat, the timestamp

A scheduled job is known to have run when 4 things exist: a start and finish log line with counts, a non-zero exit that reaches the error tracker, a heartbeat ping sent only on success to a monitor that alerts when it is late, and a last-successful-run timestamp with an alert on staleness.

The four parts, cheapest first:

  1. 01 Log a start line and a finish line for every run, with the job's name, a run id, the duration and what it did in numbers, such as rows checked or emails sent, in the same shape as the app's other logs.
  2. 02 Exit non-zero on failure and send the error to the error tracker, so a failed run raises the same alert a failed request would.
  3. 03 On success, and only on success, ping a heartbeat monitor that alerts when the ping is late. With the stale-timestamp alert in part 4, this is what catches a job that never started.
  4. 04 Write a last-successful-run timestamp somewhere visible, such as a row in the database, and alert when it is older than about two intervals.

The two-interval threshold in part 4 is my working rule, not a standard: long enough to forgive one slow run, short enough that a stopped hourly job is noticed within a couple of hours. The shape of each log line follows error logging best practices; how a monitor like that works, and which to choose, is under heartbeat checks for jobs that stop silently; and the stale-timestamp alert is one case of how to alert on error rate spikes.

How to check your own app

Scheduled jobs are proven with 5 checks: an inventory that maps every job to a host and a file in the repo, a run you start yourself, a deliberate failure that reaches a person, a disabled schedule that raises a late alert, and a double start that ends at a lock or leaves one result.

Start in the browser: open the host’s scheduled-jobs page and read what is listed there before you open a terminal. On Vercel it is the project’s Settings, then Cron Jobs; on Render, each cron job’s Runs page shows run history and logs; on Supabase, it is Integrations, then Cron.

  1. 01 Write the inventory: every scheduled job, its line, the host that runs it and the file in the repository that defines it. A job nobody can find in the repository fails this check. Evidence: the inventory, dated.
  2. 02 Run each job once yourself in the environment its scheduler uses (on a crontab host, a shell with cron's bare environment) and read its start and finish lines. Evidence: the two lines.
  3. 03 On staging, give one job a wrong credential and confirm the failure reaches the error tracker and that a person is told. Evidence: the alert, with the time it arrived.
  4. 04 On staging, disable the schedule for one job and wait at least one interval plus the monitor's grace period before judging. The heartbeat or stale-timestamp alert should arrive. Evidence: the timing.
  5. 05 On a crontab host with flock -n, start the same job twice at once and confirm the second exits at the lock. On a platform scheduler, run the job twice instead and confirm there is one result: the idempotency check. Evidence: the exit code, or the single result.

For check 3, the channel depends on the tracker’s plan: Sentry’s free Developer plan lists alerts and notifications via email, and its Team plan adds API and third-party integrations. For check 5, a second flock with -n exits with status 1 by default when the lock is held, and -E changes that number.

Deliverable 6.7 of the Production Hardening Sprint, Reliable job execution, is verified this way: Replay jobs, exhaust retries, and verify one intended result plus a recoverable failure record.

Where the sprint fits

No sprint deliverable is called cron, but three cover the jobs on this page. Deliverable 5.9, Scheduled reconciliation: run recurring reconciliation and alert the team to billing discrepancies. Under 6.7, Reliable job execution, we make jobs idempotent and provide a dead-letter or failed-job path with a recovery procedure. And 8.3, Operational threshold alerts, means we alert on error spikes, latency, connection pressure, and queue backlog. The app’s current framework and hosting setup are the starting point, and components are refactored or replaced where the production work requires it. After handover, the cover is 14 calendar days of fixes for defects in the delivered sprint work. All three are listed, with how each is verified, on the published scope.

Common questions about scheduled jobs

Is cron outdated?

No. On a Linux server you run yourself, a crontab line is still how you schedule a recurring job. What changed is where apps run: on serverless and managed hosts the platform’s scheduler takes its place, and on Linux hosts that use systemd, a timer unit with OnCalendar= is the other option.

What is the difference between EventBridge schedulers and rules?

EventBridge Scheduler is the one AWS recommends for invoking targets on a schedule, and it says Scheduler “offers improved scalability over EventBridge scheduled rules, with a wider set of target API operations and AWS services.” Scheduled rules, a legacy feature in AWS’s words, take a rate or a cron expression and run in UTC+0. Scheduler adds one-time schedules and time zones you choose, and a schedule can set a flexible time window, a retry limit and a maximum retention time for failed triggers.

How can I list all cron jobs?

Run crontab -l for your own jobs and crontab -u <user> -l for another user’s, then read /etc/crontab and the files in /etc/cron.d/, where system jobs live. Jobs on a hosting platform are listed in its dashboard, not in any crontab.

How to run a cron manually?

Copy the command from the crontab line and run it in a shell stripped to what cron provides: SHELL set to /bin/sh, HOME and LOGNAME from the account, and nothing from your profile. That is how you find the path and variable problems before cron does. On Render you can press Trigger Run on the job’s Runs page, and on Netlify you select the scheduled function and click Run now.

Is cron free?

Yes: cron runs on the Linux server you already pay for, with no separate charge. Platform schedulers are limited by plan instead: Vercel’s Hobby plan allows cron jobs once per day, Render has a minimum monthly charge of $1 per cron job service, and EventBridge Scheduler’s Free Tier covers 14,000,000 invocations per month.