How to Monitor Cron Jobs in Go
A panic in a scheduled goroutine takes down the whole binary. robfig/cron has a recovery wrapper, and it is not on by default.
Monitoring scheduled work in Go is mostly about two things: making sure a panic in one job does not take the process down, and putting the ping somewhere a panic cannot skip.
Go's default behaviour on both counts is unhelpful, and the fix is short.
A panic in a goroutine kills the program
This is the Go-specific trap. An unrecovered panic in any goroutine terminates the entire process, and scheduler libraries run each job in its own goroutine.
One nil map write in a nightly report brings down the binary, which takes every other scheduled job with it. If the process is supervised it restarts, and the job that panicked will panic again at the next tick.
robfig/cron ships a recovery wrapper and it is not enabled unless you ask for it.
SkipIfStillRunning is the other one worth having. Without it, a job that overruns its interval will have a second copy started alongside it, and for anything writing to a database that is a real problem.
Note that a skipped run produces no ping, so your monitor will report a missed heartbeat. That is the correct outcome: the job genuinely did not run.
defer puts the ping where panics cannot skip it
A deferred function runs during panic unwinding, which makes it the right place for the report. Combine it with a named error return and a recover, and one helper covers success, error and panic.
The context timeout matters as much as the panic recovery. A job blocked on a database call with no deadline runs forever, never pings, never errors, and holds the SkipIfStillRunning lock so every subsequent run is skipped too.
Six or five fields, depending on the constructor
robfig/cron v3 defaults to the standard five-field format, but the library historically supported an optional leading seconds field and plenty of code still assumes six.
A five-field expression parsed by a six-field parser shifts every field by one position. Your daily 03:00 job silently becomes something else entirely, and it will look correct in code review.
The in-process trade-off
A Go scheduler lives inside your binary. That gives you shared config, typed access to the rest of your code, and no process startup cost per run.
It also means a deploy, an OOM kill, or a crash in unrelated code stops every schedule at once. The binary restarting brings them back, and any run due during the gap is simply lost, because cron libraries do not backfill missed ticks by default.
For work that must not be missed, run the job as a separate binary invoked by system cron or a Kubernetes CronJob, and keep the in-process scheduler for work where a skipped run is recoverable.
Related guides
The same in-process trade-off in JavaScript is covered in monitoring cron jobs in Node.js.