The schedules you actually need.
Three shapes cover almost every recurring job: a fixed interval, a wall-clock time in a real timezone, and a job that re-arms itself only once the previous run finished.
Common Tasks
Section titled “Common Tasks”Run every N milliseconds
Section titled “Run every N milliseconds”Use every instead of a cron pattern when you want fixed-rate scheduled slots.
The next slot is anchored to the previous scheduled time, not to completion of
the generated job. With the default preventOverlap: true, a still-active
previous generation blocks another one from being enqueued:
await queue.upsertJobScheduler('heartbeat', { every: 60000, // every minute limit: 100, // optional: stop after 100 runs}, { data: { check: 'health' },});await queue.upsertJobScheduler('heartbeat', { every: 60000, // every minute limit: 100, // optional: stop after 100 runs}, { data: { check: 'health' },});queue.upsert_job_scheduler("heartbeat", {"every": 60000, # every minute "limit": 100}, # optional: stop after 100 runs {"data": {"check": "health"}})$queue->upsertJobScheduler('heartbeat', [ 'every' => 60000, // every minute 'limit' => 100, // optional: stop after 100 runs], [ 'data' => ['check' => 'health'],]);err := queue.UpsertJobScheduler("heartbeat", bunqueue.SchedulerRepeat{ EveryMs: 60000, // every minute Limit: 100, // optional: stop after 100 runs }, bunqueue.SchedulerTemplate{ Data: map[string]any{"check": "health"}, })queue.upsert_job_scheduler( "heartbeat", SchedulerRepeat { every_ms: Some(60_000), // every minute limit: Some(100), // optional: stop after 100 runs ..Default::default() }, SchedulerTemplate { data: Value::Map(vec![(Value::from("check"), Value::from("health"))]), ..Default::default() },)?;# every minute; optional limit: stop after 100 runs:ok = Bunqueue.Queue.upsert_scheduler(queue, "heartbeat", %{every: 60_000, limit: 100}, %{data: %{check: "health"}} )CLI equivalent: bunqueue cron add heartbeat -q system -d '{"check":"health"}' -e 60000.
Schedule in a specific timezone
Section titled “Schedule in a specific timezone”Pass an IANA timezone (like Europe/Rome or America/New_York) and the pattern is evaluated in that timezone, daylight saving included:
// 6 PM New York time, weekdays onlyawait queue.upsertJobScheduler('end-of-day', { pattern: '0 18 * * 1-5', timezone: 'America/New_York',}, { name: 'end-of-day', data: { type: 'summary' },});// 6 PM New York time, weekdays onlyawait queue.upsertJobScheduler('end-of-day', { pattern: '0 18 * * 1-5', tz: 'America/New_York',}, { name: 'end-of-day', data: { type: 'summary' },});# 6 PM New York time, weekdays onlyqueue.upsert_job_scheduler("end-of-day", {"pattern": "0 18 * * 1-5", "tz": "America/New_York"}, {"name": "end-of-day", "data": {"type": "summary"}})// 6 PM New York time, weekdays only$queue->upsertJobScheduler('end-of-day', [ 'pattern' => '0 18 * * 1-5', 'tz' => 'America/New_York',], [ 'name' => 'end-of-day', 'data' => ['type' => 'summary'],]);// 6 PM New York time, weekdays onlyerr := queue.UpsertJobScheduler("end-of-day", bunqueue.SchedulerRepeat{ Pattern: "0 18 * * 1-5", Timezone: "America/New_York", }, bunqueue.SchedulerTemplate{ Name: "end-of-day", Data: map[string]any{"type": "summary"}, })// 6 PM New York time, weekdays onlyqueue.upsert_job_scheduler( "end-of-day", SchedulerRepeat { pattern: Some("0 18 * * 1-5".into()), timezone: Some("America/New_York".into()), ..Default::default() }, SchedulerTemplate { name: Some("end-of-day".into()), data: Value::Map(vec![(Value::from("type"), Value::from("summary"))]), ..Default::default() },)?;# 6 PM New York time, weekdays only:ok = Bunqueue.Queue.upsert_scheduler(queue, "end-of-day", %{pattern: "0 18 * * 1-5", timezone: "America/New_York"}, %{name: "end-of-day", data: %{type: "summary"}} )From the CLI, pass --timezone (-z):
bunqueue cron add daily-report -q reports -d '{"type":"daily"}' \ -s "0 9 * * *" -z Europe/RomeRepeat a job after each completion
Section titled “Repeat a job after each completion”For simple repetition tied to job completion, adding a job with the repeat
option also works: the job re-enqueues itself every milliseconds after each
successful completion. Here limit is the number of successors, so total
executions are the initial job plus at most limit repeats. Failed terminal
jobs do not create a successor.
await queue.add('sync', { source: 'crm' }, { repeat: { every: 30000, limit: 10 } });await queue.add('sync', { source: 'crm' }, { repeat: { every: 30000, limit: 10 } });queue.add("sync", {"source": "crm"}, repeat={"every": 30000, "limit": 10})$queue->add('sync', ['source' => 'crm'], ['repeat' => ['every' => 30000, 'limit' => 10]]);queue.Add("sync", map[string]any{"source": "crm"}, bunqueue.JobOptions{"repeat": map[string]any{"every": 30000, "limit": 10}})queue.add("sync", Value::Map(vec![(Value::from("source"), Value::from("crm"))]), JobOptions { repeat: Some(Value::Map(vec![ (Value::from("every"), Value::from(30000)), (Value::from("limit"), Value::from(10)), ])), ..Default::default() })?;{:ok, _job} = Bunqueue.Queue.add(queue, "sync", %{source: "crm"}, repeat: %{every: 30_000, limit: 10} )repeat.pattern on queue.add uses the same cron parser as named schedulers.
The directly added generation runs first; every successful completion creates
the next generation at the calculated cron deadline. tz, startDate,
endDate, offset, limit, and immediately stay attached to the chain, and
the chain survives a SQLite-backed broker restart.
An offset shifts each cron tick without skipping a tick whose shifted deadline
is still in the future. Negative offsets advance to the next future shifted
tick instead of creating a zero-delay loop. For interval repeats, the offset
sets the first successor phase and later generations continue on every
cadence; immediately applies only to the directly added generation.
Where to go next
Section titled “Where to go next”| Job Schedulers from the Queue | Named repeatable schedules on the Queue object |
| Cron Jobs in Bun | Your first schedule, in every SDK and from the CLI |
| Cron Reference | Expression syntax, every scheduler option, MCP |