Cron schedule
Cron every 30 seconds
Standard cron can’t do it: its smallest unit is one minute. The usual workaround is two crontab lines, * * * * * job.sh and * * * * * sleep 30; job.sh; a systemd timer with OnCalendar=*:*:0/30 or a 6-field scheduler like node-cron (*/30 * * * * *) does it natively.
| Expression | None in standard 5-field cron |
|---|---|
| Runs | 2,880 times a day |
| systemd OnCalendar | *:*:0/30 |
How it works
The cron daemon wakes up once a minute, checks every crontab line against the current minute, hour, day, month and weekday, and starts what matches. There is no seconds field in the 5-field format, so nothing in the expression can say “at second 30”. That’s a design limit, not a missing feature in your cron: cronie, Debian’s cron, BusyBox crond and the hosted schedulers all work in whole minutes.
The two-line trick works because both entries fire at the top of the minute and the second one waits 30 seconds before doing anything. It is fine for lightweight checks. It gets messy if the job’s runtime varies, because the gaps stop being 30 seconds and two copies can run at once.
If you control the machine, a systemd timer is the clean answer: OnCalendar=*:*:0/30 means seconds 0 and 30 of every minute, and AccuracySec=1s stops systemd from batching the wakeups. Inside an application, a scheduler with a seconds field (node-cron, Spring, Quartz) or a plain loop with a sleep is simpler than cron.
Next runs
As on GitHub Actions without timezone, Vercel, Cloudflare and Kubernetes with timeZone "Etc/UTC".
| Run | Your time | UTC |
|---|---|---|
| 1 | Calculating… | |
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
Ready-to-paste versions
The same schedule for each scheduler, with what differs on each one.
crontab (Linux, macOS, BSD)
# cron can’t go below one minute: start two copies per minute,
# the second one 30 seconds late.
* * * * * /usr/local/bin/job.sh
* * * * * sleep 30; /usr/local/bin/job.sh- Both lines fire at second 0 of each minute;
sleep 30delays the second copy. If the job itself takes a few seconds, the gap between runs is uneven, and if it takes more than 30 seconds the two copies overlap. Addflock -n /tmp/job.lockin front of the command to prevent that.
GitHub Actions(Not supported)
- Not possible: GitHub’s shortest schedule interval is 5 minutes.
Kubernetes CronJob(Not supported)
- Not possible:
spec.scheduleuses 5-field cron, so one minute is the floor, and starting a Pod every 30 seconds would be wasteful anyway. Run a Deployment with a loop instead.
Vercel Cron Jobs(Not supported)
- Not possible: Vercel cron expressions have 5 fields, and the most frequent schedule is once per minute on Pro and Enterprise.
Cloudflare Workers(Not supported)
- Not possible with Cron Triggers, which use 5 fields. A Durable Object alarm that re-arms itself 30 seconds out can do it.
node-cron (Node.js)
import cron from 'node-cron';
// second 0 and second 30 of every minute
cron.schedule('*/30 * * * * *', async () => {
await runJob();
}, { noOverlap: true });- With six fields the first one is seconds, so
*/30 * * * * *fires at :00 and :30.noOverlap: trueskips a tick if the previous run hasn’t finished.
systemd timer
# /etc/systemd/system/job.timer
[Unit]
Description=Run job.service every 30 seconds
[Timer]
OnCalendar=*:*:0/30
AccuracySec=1s
[Install]
WantedBy=timers.target
# /etc/systemd/system/job.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/job.sh
# systemctl daemon-reload && systemctl enable --now job.timer- Check it with
systemd-analyze calendar '*:*:0/30', which prints the normalized form and the next elapse.AccuracySecdefaults to 1min, so the start can drift by up to a minute. AccuracySec=1smatters here: the default of 1min lets systemd coalesce the wakeups and your 30-second timer would drift toward once a minute.
Spring @Scheduled and Quartz
@Scheduled(cron = "*/30 * * * * *")
public void runJob() { }
// Or, for a fixed period instead of clock-aligned times:
@Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS)
// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0/30 * * * * ?")- Spring and Quartz both have a seconds field, so this is a one-liner.
fixedRatecounts from application start rather than from the wall clock.
Pitfalls
Six fields mean different things in different tools
node-cron and Spring put seconds first, so */30 * * * * * is every 30 seconds there. Paste the same string into a 5-field crontab and it’s rejected; drop the first field and * * * * * is every minute. Quartz also puts seconds first but wants ? in a day field: 0/30 * * * * ?.
Overlap when a run takes longer than 30 seconds
Neither cron line knows about the other. If a run takes 40 seconds, the next one starts before it ends. Guard the command with flock -n /tmp/job.lock job.sh so the late copy exits instead of stacking up.
Starting a process twice a minute adds up
That’s 2,880 process starts a day, each loading config and opening fresh connections. If the job polls for work, a long-running worker that sleeps 30 seconds between iterations, or a queue, is usually cheaper and reacts faster.
Frequently asked questions
- Can a crontab run a job every 30 seconds?
- Not with one line. The 5-field format has no seconds, so the finest schedule is every minute. Use two lines, the second prefixed with sleep 30, or move to a systemd timer or a scheduler with a seconds field.
- What is the systemd timer for every 30 seconds?
- OnCalendar=*:*:0/30 with AccuracySec=1s. Without the AccuracySec line systemd may delay each elapse by up to a minute to batch wakeups.
- Is */30 * * * * every 30 seconds?
- No. In a 5-field crontab the first field is minutes, so */30 * * * * runs at minutes 0 and 30, every half hour. Only 6-field schedulers such as node-cron or Spring read */30 * * * * * as seconds.
- Can GitHub Actions or Vercel run every 30 seconds?
- No. GitHub’s shortest schedule is every 5 minutes and Vercel’s is every minute (Pro and Enterprise). Both use 5-field cron.
Last reviewed by Arielton Oberek.