Cron doesn't do seconds. The smallest unit in a cron expression is one minute, and no amount of syntax tricks changes that. If you write */30 * * * *, you get every 30 minutes, not every 30 seconds. The standard crontab has five fields, and none of them represent seconds.
But running a task every 30 seconds is a real need. Health checks against a local service, queue workers that need near-instant pickup, sensor reads, cache warming. People run into this a lot, and the workaround depends on your environment. Three approaches that actually work in production.
The sleep 30 trick (simplest approach)
Run the same command twice per minute: once immediately, once after a 30-second delay. Two crontab entries handle it:
* * * * * /usr/bin/php /opt/scripts/check-queue.php
* * * * * sleep 30 && /usr/bin/php /opt/scripts/check-queue.php
First line fires at second 0 of every minute. Second line also fires at second 0, but sleep 30 pauses it for half a minute before running the actual command. && means the script only executes if the sleep completes successfully (it always does, but it's good practice).
Result: your script runs at :00 and :30 of every minute. 2,880 executions per day.
A few things to watch:
- Overlap. If your script takes more than 30 seconds, the next invocation starts while the previous one is still running. Protect against this with flock:
* * * * * /usr/bin/flock -n /tmp/check-queue.lock /usr/bin/php /opt/scripts/check-queue.php
* * * * * sleep 30 && /usr/bin/flock -n /tmp/check-queue.lock /usr/bin/php /opt/scripts/check-queue.php
- Log noise. 2,880 cron log entries per day per job. We once had a staging server with four sub-minute jobs running this way. Syslog hit 2 GB in under a week. Redirect output to a dedicated file with
>>and set up log rotation before you forget. - Exit code swallowing. If the script fails, cron won't retry. The next run in 30 seconds starts fresh regardless. Good or bad depending on your use case, but worth knowing.
A wrapper script with a loop
Instead of two crontab lines, you can run a single entry per minute that loops internally:
* * * * * /opt/scripts/run-every-30s.sh
Where run-every-30s.sh looks like:
#!/bin/bash
/usr/bin/php /opt/scripts/check-queue.php &
sleep 30
/usr/bin/php /opt/scripts/check-queue.php &
Backgrounding the first execution with & lets the sleep start immediately. Whole thing finishes in about 30 seconds, well before the next minute's invocation. Keeps your crontab cleaner when you have multiple sub-minute jobs.
For tighter intervals like every 10 or 15 seconds, extend the pattern:
#!/bin/bash
for i in 0 30; do
sleep $i && /usr/bin/php /opt/scripts/check-queue.php &
done
Same result, slightly more maintainable if you later change the interval.
systemd timers (no cron at all)
If you're on a system running systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+, basically anything modern), timers can handle sub-minute scheduling without the sleep workaround. More boilerplate to set up, but cleaner once it's running.
You need two files. First, a service unit at /etc/systemd/system/check-queue.service:
[Unit]
Description=Check queue processor
[Service]
Type=oneshot
ExecStart=/usr/bin/php /opt/scripts/check-queue.php
Then a timer unit at /etc/systemd/system/check-queue.timer that tells systemd how often to trigger it:
[Unit]
Description=Run queue check every 30 seconds
[Timer]
OnBootSec=30s
OnUnitActiveSec=30s
AccuracySec=1s
[Install]
WantedBy=timers.target
Enable and start:
sudo systemctl enable --now check-queue.timer
Check status:
systemctl list-timers --all | grep check-queue
Here's the important part: OnUnitActiveSec=30s triggers the service 30 seconds after the last run completes. Not 30 seconds after it started. So if your script takes 10 seconds, the actual interval between starts is 40 seconds. For most use cases that's fine. For long-running scripts it's actually better than cron's approach, because you never get overlapping runs.
One detail easy to miss: AccuracySec=1s. Without it, systemd batches timer events within a 1-minute window to save power (a holdover from laptop power management). Setting accuracy to 1 second disables that grouping. Skip it and your "every 30 seconds" might drift to every 45 or 60.
Which approach to pick
| Method | Pros | Cons |
|---|---|---|
| Two crontab lines + sleep | Works everywhere, zero setup | Messy with many jobs, overlap risk |
| Wrapper script | Clean crontab, flexible intervals | Another file to maintain |
| systemd timer | Native sub-minute support, no overlap | Linux only, more boilerplate |
For one or two jobs, the sleep trick is fine. We've seen it run for years on production servers without issues. When you have five or more sub-minute jobs, systemd timers are cleaner. The wrapper script sits in between: pragmatic for three or four jobs where you don't want to touch systemd.
Do you actually need every 30 seconds?
Worth asking before you wire any of this up. A 30-second interval means 2,880 runs per day. Each one spawns a process, potentially opens a database connection, writes to a log. On a $5 VPS running a WordPress site with limited CPU, that overhead adds up faster than you'd expect.
Common cases where 30 seconds makes sense:
- Queue processing where jobs need near-instant pickup (payment callbacks, webhook delivery)
- Health checks against local services where a 1-minute gap is too long for your SLA
- Sensor or metrics collection where sampling rate matters for accuracy
Cases where every minute or every 5 minutes is usually enough:
- Cache invalidation (most caches handle TTL natively)
- Email queue processing (30 seconds of delay is invisible to the recipient)
- Log rotation, cleanup scripts, report generation
If a 1-minute gap is genuinely too long but a full daemon feels like overkill, 30 seconds is a reasonable middle ground. Once you start wanting every 5 or 10 seconds though, you're really building a daemon whether you call it one or not. At that point, a proper background process managed by systemd, Supervisor, or PM2 is both more honest about what you're doing and more reliable than layering hacks on top of cron.
Keeping track of 2,880 daily runs
A job that runs every hour and misses once — no big deal, the next run picks it up. At 30-second intervals the math is different: a broken path after a PHP update, a full disk at 3 AM, a stuck lock file from a crashed process — and within 10 minutes you've already skipped 20 runs. If that job processes a payment queue or checks service health, the unfinished work piles up fast.
Cron logs (grep CRON /var/log/syslog) tell you the command was called, but not whether it finished successfully. A simpler approach: add a heartbeat ping — a small HTTP request — to the end of your script. As long as those pings keep arriving on schedule, everything is working. The moment they stop, you get an alert before the unprocessed work stacks up.
If the job already calls an HTTP endpoint (API sync, webhook delivery, health probe), an external scheduler can handle both the triggering and the monitoring in one place. WatchCron's Cloud Cron works this way, with 10+ alert channels built in — though the minimum interval there is one minute, so true 30-second scheduling still needs one of the local methods above.
Related schedules
| Schedule | Method | Runs per day |
|---|---|---|
| Every 10 seconds | Loop script or systemd timer | 8,640 |
| Every 15 seconds | Loop script or systemd timer | 5,760 |
| Every 30 seconds | sleep 30 trick or systemd timer | 2,880 |
| Every minute | * * * * * | 1,440 |
| Every 5 minutes | */5 * * * * | 288 |
| Every 10 minutes | */10 * * * * | 144 |
| Every 30 minutes | */30 * * * * | 48 |
| Every hour | 0 * * * * | 24 |
More patterns in the cron expression examples library. For syntax questions, the cron expression cheat sheet covers all five fields with examples.
Monitor your cron jobs — even the fast ones
20 monitoring checks and three Cloud Cron jobs on the free plan. No credit card.