Cron syntax and examples
How to write pg_cron schedules on SupaHost. The five cron fields, ranges, lists and steps, sub-minute "N seconds" schedules, UTC times, and a table of ready-to-use examples.
A pg_cron schedule is either a standard five-field cron expression or an interval in seconds. Both are written as text in cron.schedule() or in the Schedule field of the dashboard's Cron tab. All times are UTC on SupaHost.
The five fields
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-6, Sunday = 0)
│ │ │ │ │
* * * * *
A job runs whenever the current UTC time matches every field. A * means "any value".
Building blocks
| Syntax | Meaning | Example | Reads as |
|---|---|---|---|
* |
Any value | * * * * * |
Every minute |
n |
Exactly n | 15 4 * * * |
04:15 every day |
a-b |
Range | 0 9-17 * * * |
On the hour, 09:00 to 17:00 |
a,b,c |
List | 0 8,12,18 * * * |
08:00, 12:00 and 18:00 |
*/n |
Every n | */10 * * * * |
Every 10 minutes |
a-b/n |
Every n within a range | 0 8-20/4 * * * |
08:00, 12:00, 16:00, 20:00 |
When both day of month and day of week are restricted (neither is *), the job runs when either matches. 0 9 1 * 1 runs at 09:00 on the 1st of each month and also every Monday. To run on "the first Monday", schedule it every Monday and check the date in SQL.
Examples
| Schedule | When it runs (UTC) | Typical job |
|---|---|---|
* * * * * |
Every minute | Process a queue, send due notifications |
*/5 * * * * |
Every 5 minutes | Sync with an external API |
0 * * * * |
At the start of every hour | Refresh a materialized view |
30 2 * * * |
Every day at 02:30 | Delete expired rows |
0 3 * * 0 |
Sundays at 03:00 | Weekly cleanup or report |
0 9 * * 1-5 |
Weekdays at 09:00 | Send a morning digest |
0 0 1 * * |
Midnight on the 1st of each month | Generate monthly invoices |
0 6 1 1,4,7,10 * |
06:00 on the first day of each quarter | Quarterly summaries |
15,45 * * * * |
At :15 and :45 every hour | Spread two runs across the hour |
30 seconds |
Every 30 seconds | Short polling work |
The dashboard's Schedule presets cover the common cases: Every minute, Every 5 minutes, Hourly, Daily at 03:00 UTC and Every 30 seconds.
Sub-minute schedules
Cron expressions can't go below one minute. For more frequent jobs, pg_cron accepts an interval in seconds, from 1 to 59:
select cron.schedule(
'drain-email-queue',
'20 seconds',
$$ select net.http_post(
url := (select decrypted_secret from vault.decrypted_secrets where name = 'project_url')
|| '/functions/v1/email-worker',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'service_role_key')
)
); $$
);
Seconds schedules are intervals, not clock times, so use them for polling-style work rather than "at exactly :30". Keep each run short: a job never overlaps with itself, so a slow run delays the next one.
Working with UTC
Schedules are evaluated in UTC, so convert local times before you write them:
| You want | Local offset | Write |
|---|---|---|
| 08:00 in London (winter, UTC+0) | +0 | 0 8 * * * |
| 08:00 in Berlin (winter, UTC+1) | +1 | 0 7 * * * |
| 08:00 in New York (winter, UTC-5) | -5 | 0 13 * * * |
| 09:00 in Tokyo (UTC+9, no daylight saving) | +9 | 0 0 * * * |
Cron doesn't follow daylight saving changes. A job at 0 7 * * * runs at 08:00 Berlin time in winter and 09:00 in summer. If the local time matters exactly, either accept the one-hour shift, or schedule the job hourly and check the local time in SQL:
select cron.schedule(
'berlin-morning-digest',
'0 * * * *',
$$
do $job$
begin
if extract(hour from now() at time zone 'Europe/Berlin') = 8 then
insert into public.digest_runs (ran_at) values (now());
-- ...build and send the digest here
end if;
end
$job$;
$$
);
Note the different dollar-quote tag ($job$) inside the outer $$ ... $$, so the two don't clash.
Checking a schedule
Before relying on a new schedule, confirm it registered the way you expect:
select jobname, schedule, active from cron.job where jobname = 'berlin-morning-digest';
Then watch the first few runs in Dashboard → your project → Cron tab → Recent runs or in cron.job_run_details. See Monitoring and debugging cron jobs.
If an expression isn't valid, cron.schedule raises an error and no job is created.
Spreading the load
Many jobs scheduled at 0 * * * * or 0 0 * * * all start at the same moment, on the same server as your API and database. Offset them by a few minutes (7 * * * *, 23 * * * *) so they don't compete. Also avoid 03:00 to 05:00 UTC for heavy jobs if your instance has automatic upgrades turned on, since that's the maintenance window.