Metronome
A Nim scheduler library for interval, cron, timer, and one-shot jobs.
Schedule By Every
Example usage::
import metronome, times, asyncdispatch
metronome:
every(seconds=1, id="tick", async=true):
echo("async tick ", now())
await sleepAsync(2000)
every(seconds=1, id="sync tick"):
echo("sync tick ", now())
The code enables you:
- Schedule an async proc every second.
- Schedule a thread-backed proc every second.
Run::
nim c --threads:on -r examples/example_basic_intervals.nim
Note:
- Compile applications that import metronome with --threads:on. Jobs without async=true run in worker threads and must satisfy Nim's thread and GC-safety rules. Async jobs run on the event loop and should not make blocking calls.
- Exceptions from thread-backed jobs are not propagated through job futures.
- The library schedules all jobs at a regular interval, but it'll be impacted by your system load.
Schedule By Cron
You can set minute, hour, day_of_month, month, day_of_week, and year in the cron() call. Omitted fields default to *, and scheduling has one-minute resolution.
Supported field ranges:
- minute: 0-59
- hour: 0-23
- day_of_month: 1-31
- month: 1-12 or jan-dec
- day_of_week: 1-7 or mon-sun, where Monday is 1
- year: 1970-9999
Fields are case-insensitive. All fields accept *, comma-separated lists, inclusive ranges, and steps such as */5, 2/3, or 1-10/2. day_of_month also accepts L or last for the last day of the month. day_of_week accepts dL for the last named weekday and d#n for the nth named weekday, such as friL and mon#3.
When both day_of_month and day_of_week are restricted, a match on either field is selected. The parser recognizes ? and W, but the next-run evaluator does not implement them. Direct newCron calls expose second, but the current next-run calculation does not evaluate it.
Example usage::
import metronome, times, asyncdispatch
metronome:
cron(minute="*/1", hour="*", day_of_month="*", month="*", day_of_week="*", id="tick"):
echo("tick", now())
cron(minute="*/1", hour="*", day_of_month="*", month="*", day_of_week="*", id="atick", async=true):
echo("tick", now())
await sleepAsync(3000)
The code enables you:
- Schedule thread proc every minute.
- Schedule async proc every minute.
- Pass timezone=utc() or another Timezone to evaluate cron fields in that zone.
Run::
nim c --threads:on -r examples/example_cron_scheduler.nim
Named IANA Timezones
Import metronome/timezones separately to resolve an embedded IANA name. The optional database is not included when an application imports only metronome::
import metronome, metronome/timezones
import asyncdispatch, times
let zone = namedTimezone("Europe/Amsterdam")
scheduler localSched:
cron(hour="9", minute="0", timezone=zone, async=true):
echo now().inZone(zone)
Cron fields stay at the requested local wall-clock hour across DST changes. See the generated metronome/timezones module documentation for database version reporting, supported-name lookup, and local-time edge-case behavior.
Schedule by Timer
Import metronome/timers separately to schedule jobs with systemd-style calendar expressions. The timer parser and embedded timezone database stay out of applications that import only metronome::
import metronome
import metronome/timers
scheduler timerSched:
timer(
onCalendar="*-*-* 02:00:00 Europe/Amsterdam",
id="nightly",
async=true
):
echo "Running nightly"
Syntax: Year-Month-Day Hour:Minute:Second TimeZone. Fractional seconds retain one-microsecond calendar resolution, but event-loop dispatch remains best-effort and normally has millisecond-scale accuracy. See the generated metronome/timers documentation for supported systemd forms and compatibility boundaries.
Note:
- Compile applications with --threads:on.
- The library schedules all jobs at a regular interval, but it'll be impacted by your system load.
Schedule Once
Use at(time=...) inside a metronome or scheduler block to schedule a job one time.::
import metronome, times, asyncdispatch
metronome:
at(time=now()+initDuration(minutes=5), id="warm-cache", async=true):
echo("warming cache")
One-shot jobs stop after their first launch.
Throttling
By default, only one instance of the job is to be scheduled at the same time. If a job hasn't finished but the next run time has come, the next job will not be scheduled.
You can allow more instances by specifying throttle=. For example::
import metronome, times, asyncdispatch
metronome:
every(seconds=1, id="async tick", throttle=2, async=true):
echo("async tick ", now())
await sleepAsync(2000)
every(seconds=1, id="tick", throttle=2):
echo("sync tick ", now())
Customize Scheduler
Sometimes, you want to run the scheduler in parallel with other libraries. In this case, you can create your own scheduler by macro scheduler and start it later.
Below is an example of running Metronome and Prologue in one process.::
import std/[asyncdispatch, logging, times]
import metronome, prologue
let fileLogger = newFileLogger("messages.log", mode=fmAppend)
scheduler mySched:
every(seconds=1, id="tick", async=true):
let tickTime = now()
echo("tick, seconds=1 ", tickTime)
fileLogger.log(lvlInfo, "1 second tick: ", tickTime)
proc hello*(ctx: Context) {.async.} =
resp "<h1>Hello, Prologue! It's alive!</h1>"
proc main() {.async.} =
asyncCheck mySched.start()
let settings = prologue.newSettings()
var app = newApp(settings=settings)
app.addRoute("/", hello)
await app.runAsync()
when isMainModule:
waitFor main()
Set Start Time and End Time
You can limit the schedules running in a designated range of time by specifying startTime and endTime.
For example::
import metronome, times, asyncdispatch, os
scheduler demoSetRange:
every(
seconds=1,
id="tick",
startTime=initDateTime(2019, 1, 1),
endTime=now()+initDuration(seconds=10)
):
echo("tick", now())
when isMainModule:
waitFor demoSetRange.start()
Parameters startTime and endTime can be used independently. For example, you can set startTime only, or set endTime only.
Calculate Next Run Times
Use fireTime to inspect the next scheduled run without starting a scheduler. This is useful for tests, dashboards, and checking interval or cron behavior deterministically.::
import metronome, times, options, asyncdispatch
proc noop(): Future[void] {.async.} = discard
let current = dateTime(2026, mJan, 1, 12, 35, 0, 0, utc())
let beater = initBeater(
initTimeInterval(minutes=10),
noop,
startTime=some(dateTime(2026, mJan, 1, 12, 0, 0, 0, utc()))
)
echo beater.fireTime(none(DateTime), current).get()
Error Handling
Schedulers keep running when a scheduled async job fails. Failed job futures are recorded on the beater and can be passed to either a scheduler-level error handler or a job-level error handler. Job-level handlers take precedence. Error handlers are supported for async jobs only; thread-backed sync jobs do not propagate exceptions through their returned futures.
Example usage::
import metronome, asyncdispatch, times
proc handleSchedulerError(fut: Future[void]) {.gcsafe.} =
echo("job failed: ", fut.readError().msg)
proc handleJobError(fut: Future[void]) {.gcsafe.} =
echo("specific job failed: ", fut.readError().msg)
let sched = initScheduler(newSettings(errorHandler=handleSchedulerError))
sched.register(initBeater(
initTimeInterval(seconds=1),
proc(): Future[void] {.async.} =
raise newException(ValueError, "boom"),
id="failing-job",
errorHandler=handleJobError
))
asyncCheck sched.start()
The every and cron macros also support job-level handlers on async jobs using onError=::
scheduler sched:
every(seconds=1, id="failing-job", async=true, onError=handleJobError):
raise newException(ValueError, "boom")
Use lastError(id), lastErrorAt(id), and failures(id) to inspect failure state for a registered job.
Interval Jitter
Interval jobs can add a non-negative random delay to each computed run time with jitter. This is useful when many jobs or application instances would otherwise launch at the same instant. Jitter is only supported for interval schedules, not cron schedules.
Example usage::
import metronome, asyncdispatch, times
scheduler sched:
every(minutes=5, id="spread-out", async=true, jitter=initTimeInterval(seconds=30)):
echo("tick ", now())
Direct initBeater calls accept the same jitter parameter::
let beater = initBeater(
initTimeInterval(minutes=5),
proc(): Future[void] {.async.} = discard,
id="spread-out",
jitter=initTimeInterval(seconds=30)
)
Job Controls
Schedulers can pause, resume, and stop registered jobs by id. Anonymous jobs can still be registered, but ID-based controls only work when an id uniquely identifies one registered job.
pause(id) prevents future launches for that job. Already-running job futures are not cancelled. While paused, nextRun(id) is cleared; when resumed, interval jobs schedule from the current time instead of replaying every interval missed during the pause.
resume(id) returns a paused job to normal scheduling. stop(id) permanently stops one job and clears its next run time. stopAll() permanently stops all registered jobs. The ID-based control procs return true when exactly one job matches the id and false when the id is missing, empty, or ambiguous.
Example usage::
import metronome, asyncdispatch, times
let sched = initScheduler(newSettings())
sched.register(initBeater(
initTimeInterval(seconds=10),
proc(): Future[void] {.async.} = discard,
id="tick"
))
discard sched.pause("tick")
discard sched.resume("tick")
discard sched.stop("tick")
sched.stopAll()
Exports
-
logger, BeaterAsyncProc, BeaterThreadProc, NextRunProc, Throttler, initThrottler, throttled, submit, BeaterKind, BeaterState, Beater, $, initBeater, initBeater, initBeater, initBeater, initBeater, initBeater, initBeater, initBeater, id, state, lastRun, lastRun, nextRun, nextRun, lastError, lastError, lastErrorAt, lastErrorAt, failures, failures, runningCount, runningCount, pause, pause, resume, resume, stop, stop, fireTime, fire, JobErrorHandler, Settings, newSettings, Scheduler, initScheduler, register, listJobs, jobState, stopAll, idle, start, serve, waitFor, lastRun, lastError, initBeater, lastRun, initThrottler, Settings, Scheduler, initBeater, $, Beater, JobErrorHandler, initBeater, idle, initScheduler, stop, failures, pause, initBeater, submit, register, Throttler, listJobs, initBeater, resume, waitFor, lastErrorAt, state, lastErrorAt, logger, resume, throttled, nextRun, runningCount, id, fireTime, stopAll, initBeater, BeaterKind, failures, BeaterState, BeaterThreadProc, metronome, NextRunProc, lastError, pause, fire, newSettings, runningCount, stop, jobState, BeaterAsyncProc, initBeater, nextRun, scheduler, serve, initBeater, start, initBeater, pause, initBeater, runningCount, Scheduler, initBeater, JobErrorHandler, state, resume, pause, serve, idle, Throttler, initBeater, initBeater, resume, lastErrorAt, lastRun, stopAll, fireTime, initBeater, failures, NextRunProc, scheduler, lastError, fire, newSettings, runningCount, lastErrorAt, listJobs, initBeater, start, lastRun, waitFor, id, initThrottler, Settings, initBeater, failures, submit, register, Beater, stop, initScheduler, logger, throttled, nextRun, $, BeaterKind, BeaterState, BeaterThreadProc, metronome, lastError, stop, jobState, BeaterAsyncProc, nextRun, Cron, newCron, getNext, getNext, getNext