Sub-Minute Scheduling
"Every 5 seconds" is the single most-searched cron expression that does not exist. Classic Unix crontab is minute-precision — it has no seconds field, so one minute is the hard floor. But *some* cron dialects added a seconds field, and some platforms have floors well above a minute. This page is the support matrix: which scheduler can actually run sub-minute, what its real minimum is, and the three escape hatches when it can't.
Updated
The short answer
- Standard Unix cron cannot do seconds. Five fields, minute-precision, one-minute floor. This is not a limitation you can configure away — the format has nowhere to put the value.
- Some dialects added a seconds field. Quartz, Spring’s
@Scheduled, node-cron, APScheduler, tokio-cron-scheduler and the Rustcroncrate all parse a leading seconds field and will happily fire every second. - Some platforms floor higher than a minute. GitHub Actions will not run a workflow more than once every 5 minutes, no matter what you write. Vercel Hobby caps at once per day.
- If you’re on a minute-floor runtime, stop trying to express it in cron. Use a fixed-rate loop in a long-lived process. That’s the actual answer for the majority of people who search for “cron every 10 seconds”.
Support matrix
The question “can it do seconds?” has three different answers — a real seconds field, a non-cron interval mechanism, or a flat no — and the platform floors vary more than the dialects do.
| Runtime | Seconds field | Real minimum | Sub-minute expression |
|---|---|---|---|
| Unix cron / crontab | No — 5 fields | 1 minute | — |
| Kubernetes CronJob | No — 5 fields | 1 minute | — |
| GitHub Actions | No — 5 fields | 5 minutes | — |
| AWS EventBridge Scheduler | No | 1 minute | — |
| Cloudflare Workers Cron | No — 5 fields | 1 minute | — |
| Vercel Cron | No | 1 min (Pro) / 1 day (Hobby) | — |
| Quartz | Yes — 6–7 fields | 1 second | 0/5 * * * * ? |
Spring @Scheduled | Yes — 6 fields | 1 second | */5 * * * * * |
| node-cron | Yes — 6 fields | 1 second | */5 * * * * * |
| APScheduler | Yes — second= | 1 second | CronTrigger(second="*/5") |
| tokio-cron-scheduler | Yes — 6 fields | 1 second | */5 * * * * * |
Rust cron crate | Yes — 7 fields | 1 second | */5 * * * * * * |
| robfig/cron (Go) | Optional — cron.WithSeconds() | 1 second | */5 * * * * * |
| systemd timers | N/A — OnUnitActiveSec | sub-second | OnUnitActiveSec=5s |
Two rows deserve a second look:
GitHub Actions is the one that surprises people. GitHub’s documentation is blunt about it: “The shortest interval you can run scheduled workflows is once every 5 minutes.” An expression like * * * * * or */2 * * * * is still syntactically valid — the workflow file commits and validates cleanly, so there’s no error telling you anything is wrong. It simply will not run at that cadence. Treat */5 * * * * as the floor, and don’t write a schedule whose interval you aren’t actually going to get.
On top of that, scheduled workflows are queued on shared infrastructure and are explicitly not guaranteed to fire on time — delays during peak load are common, and runs can be dropped entirely. If a scheduled job must happen, have it verify its own last-success time rather than assuming every firing landed.
systemd looks like a win but has a trap. OnUnitActiveSec=10s is a legitimate sub-minute timer, but AccuracySec defaults to 1 minute — systemd coalesces wakeups to save power and places each firing at a randomized point inside that window. Your 10-second timer can drift by nearly a minute until you add AccuracySec=1s.
Why cron has no seconds
This isn’t an oversight anyone forgot to fix — it’s structural. The original Unix cron daemon woke up once a minute, compared the current time against every crontab line, and ran what matched. A seconds field would have meant waking 60× more often on hardware where that was a real cost, to serve a use case that barely existed: in 1975, nothing needed to run every five seconds.
The format that came out of that design has exactly five fields:
┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–7)
│ │ │ │ │
* * * * *
There is no sixth position to put a seconds value in, and adding one would break every crontab on earth — a 5-field expression and a 6-field expression mean completely different things depending on which end the extra field goes on. That’s precisely why the dialects that did add seconds are mutually incompatible today.
The practical consequence: */5 * * * * means every 5 minutes, */5 * * * * * means every 5 seconds, and the only difference is a field count that’s easy to miscount. This is the single most common cron bug we see, and it fails silently — the job runs, just 60× less often than intended. Paste anything ambiguous into the cron parser and read the next fire times.
The seconds-aware dialects
They all put seconds first, and then disagree about everything else.
Quartz — 6 or 7 fields, second minute hour day-of-month month day-of-week [year]. Day-of-month and day-of-week are mutually exclusive, so one of them must be ?:
0/5 * * * * ? every 5 seconds
0/30 * * * * ? every 30 seconds
* * * * * ? every second
Note Quartz idiom prefers 0/5 (“starting at 0, every 5”) over */5. Both parse, but 0/5 is what you’ll see in the wild.
Spring @Scheduled — 6 fields, no year, no ? requirement:
@Scheduled(cron = "*/5 * * * * *") // every 5 seconds
@Scheduled(cron = "0/30 * * * * *") // every 30 seconds
// Usually better — no cron parser involved at all:
@Scheduled(fixedRate = 5000) // start-to-start
@Scheduled(fixedDelay = 5000) // end-to-start (no overlap)
node-cron — 6 fields, seconds-first. A 5-field expression still works and is treated as “at second 0” of the matching minute, which makes it easy to paste a Unix expression in and get the behaviour you expected:
import cron from "node-cron";
cron.schedule("*/5 * * * * *", () => poll());
APScheduler — keyword fields rather than a positional string, which sidesteps the miscounting problem entirely:
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
CronTrigger(second="*/5") # every 5 seconds
IntervalTrigger(seconds=5) # usually what you actually want
tokio-cron-scheduler (6 fields) and the Rust cron crate (7 fields — seconds and a trailing year) are covered in depth in the Rust cron guide. The field-count difference between them is a live porting bug: */5 * * * * * is every 5 seconds in one and invalid in the other.
Go’s robfig/cron is opt-in. By default it parses 5 fields; you get seconds only if you construct the scheduler with cron.New(cron.WithSeconds()). Forgetting that turns “every 5 seconds” into “every 5 minutes” with no error.
Three ways out
If your runtime is on the “no” side of the matrix, you have three options, roughly in order of how much we’d recommend them.
1. A fixed-rate loop in a long-lived process. This is the right answer far more often than a sub-minute cron expression is. You aren’t scheduling against the wall clock, you’re polling — so express it as polling:
while True:
do_work()
time.sleep(5)
let mut ticker = tokio::time::interval(Duration::from_secs(5));
loop {
ticker.tick().await;
do_work().await;
}
Run it under systemd, supervisor, or as a Kubernetes Deployment. No cron parser, no field-count bug, and restart semantics you control. On Kubernetes specifically this is strictly better than a CronJob would be even if CronJob could do seconds — spawning a pod every five seconds costs far more than the work inside it.
2. A seconds-aware library inside your app. If you genuinely need clock alignment — “on second 0, 15, 30, 45” rather than “every 15 seconds from whenever we started” — use the dialect for your language from the section above. Alignment matters when multiple services must act in the same window, or when a downstream system buckets by wall-clock time.
3. The sleep-offset hack, if you’re stuck on plain crontab. Covered below, with its caveats.
The sleep-offset hack
The classic crontab workaround: schedule the job every minute N times, offsetting each copy with sleep.
# Every 30 seconds
* * * * * /usr/local/bin/job
* * * * * sleep 30; /usr/local/bin/job
# Every 15 seconds
* * * * * /usr/local/bin/job
* * * * * sleep 15; /usr/local/bin/job
* * * * * sleep 30; /usr/local/bin/job
* * * * * sleep 45; /usr/local/bin/job
It works, and for a low-stakes 30-second poll on a box you don’t control it’s a reasonable pragmatic answer. Know what you’re accepting:
- The offset is approximate.
sleep 30starts when cron forks the job, not at the top of the minute. Under load, the second copy drifts. - Copies can overlap. Each entry is independent — nothing stops the
sleep 30run from starting while the on-the-minute run is still going. Addflockif that matters:* * * * * flock -n /tmp/job.lock /usr/local/bin/job. - It scales badly. Every-5-seconds means twelve crontab lines, eleven of which are sleeping processes. At that point you have written a loop, just distributed across a config file.
- Logging gets confusing. Twelve entries produce twelve mail/syslog streams for one logical job.
If you find yourself writing more than two offset lines, switch to option 1.
Why only some intervals work
Even once you’re on a seconds-aware dialect, not every “every N seconds” is expressible in one line — and the rule for which ones are is pure arithmetic. A single */N expression only gives a steady interval when N divides 60 evenly.
The step operator */N in a 0–59 field means “start at 0, keep adding N while still in range.” So */45 in the seconds field produces 0, 45, then 90 — out of range — and stops. Next minute it resets to 0 and repeats. The result is firings at :00 and :45 with gaps that alternate 45s, 15s, 45s, 15s: not an interval, a stutter. Cron matches fields against the wall clock and keeps no memory of when it last ran, so it cannot carry a leftover offset across the minute boundary.
The values of N that do work are exactly the divisors of 60:
1 2 3 4 5 6 10 12 15 20 30 60
Every one of those tiles the minute cleanly — */15 gives 0, 15, 30, 45 and wraps with a perfect 15-second gap. Everything else (7, 8, 9, 11, 13, 14, 16, 25, 35, 45, 50 …) leaves a remainder and stutters.
For a non-divisor you have two honest options:
- Enumerate the cycle. The schedule “closes” over the least common multiple of N and 60. For 45 seconds that’s 180 seconds — a 3-minute cycle spread across four schedule entries. The worked example is on the every 45 seconds page; the same trap one unit up is every 45 minutes.
- Use an interval timer.
fixedDelay,tokio::time::interval,setInterval— none of them care whether N divides 60, because they count elapsed time rather than matching clock fields. This is almost always the better answer for a non-divisor interval, and it sidesteps the divisor rule entirely.
The takeaway: if the interval you want divides 60, a one-line */N is fine. If it doesn’t, reach for a timer before you reach for a multi-entry cron cycle.
The overlap problem
Sub-minute scheduling makes a latent bug urgent. At a daily cadence, a job that occasionally takes 90 seconds is fine. At a 5-second cadence, a job that occasionally takes 6 seconds means runs start stacking — and because each one is usually holding a connection or a lock, the failure mode is a slow pile-up rather than a clean error.
Every seconds-aware scheduler gives you a way to say “don’t start the next run until this one finishes,” and the names differ:
| Runtime | Non-overlapping option |
|---|---|
| Spring | @Scheduled(fixedDelay = 5000) — waits for completion |
| APScheduler | max_instances=1 (the default) plus coalesce=True |
| node-cron | Guard with an in-flight flag; there is no built-in lock |
| Quartz | @DisallowConcurrentExecution on the job class |
| tokio-cron-scheduler | Guard with a Mutex or tokio::sync::Semaphore |
| Plain crontab | flock -n /tmp/job.lock |
The general rule: at sub-minute cadence prefer fixedDelay semantics (gap measured from the end of the last run) over fixedRate (gap measured from the start). fixedRate is what a cron expression gives you, and it’s the one that stacks. That’s one more reason the loop in option 1 — where the sleep naturally comes after the work — is usually the better shape for this kind of job.
Frequently asked questions
Can cron run every 5 seconds?
What is the minimum cron interval?
How do I run a script every 30 seconds with crontab?
Does Kubernetes CronJob support seconds?
Why does my systemd timer with OnUnitActiveSec=10s fire late?
Should I use a cron expression or a fixed-rate loop for sub-minute work?
Related
Every Second
`* * * * * *` — the fastest a cron parser will go, and why you probably want a loop.
PatternEvery 5 Seconds
`*/5 * * * * *` in Spring, `0/5 * * * * ?` in Quartz.
PatternEvery 30 Seconds
The most-requested sub-minute interval, in every dialect that supports it.
PatternEvery 45 Seconds
The non-divisor trap: `*/45` stutters, and 45 doesn't divide 60.
PatternEvery Minute
`* * * * *` — the floor for standard cron, Kubernetes, and EventBridge.
ToolCron Parser
Paste a 6-field expression and see the next fire times before you deploy it.
GuideHow Cron Expressions Work
Field-by-field: what the five standard fields mean and how they combine.
GuideCron Dialects Compared
Unix vs Quartz vs Spring vs Kubernetes — field counts and operator support.
Guidesystemd Timers
The Linux answer to sub-minute scheduling — and its AccuracySec trap.