Monitor sidekiq-cron Jobs with Cuckoo
Updated
sidekiq-cron enqueues jobs on a cron schedule from inside your Sidekiq processes. When the job raises, the Sidekiq Web UI shows it in the retry set. A job that is never enqueued shows up nowhere, though. That happens when every Sidekiq process is down, when Redis was flushed and the schedule wasn't reloaded, or when a deploy dropped the YAML file.
A heartbeat check covers both failures. The job reports to Cuckoo when it runs. If the report is late or says "failed", you get an alert.
How Cuckoo sees a sidekiq-cron job
Create one check per cron job, with the same cron expression and time zone as the job. Each check has a URL:
https://cuckoo.metastic.ch/ping/YOUR-UUID/startwhenperformbeginshttps://cuckoo.metastic.ch/ping/YOUR-UUIDwhen it finisheshttps://cuckoo.metastic.ch/ping/YOUR-UUID/failwhen it raises (a POST body of up to 10 KB is stored)
Step 1: The schedule
sidekiq-cron loads config/schedule.yml automatically. A time zone can be appended to the cron string:
# config/schedule.yml
nightly_billing:
cron: "0 3 * * * Europe/Zurich"
class: "NightlyBillingJob"
queue: default
description: "Charge renewals"
Or create jobs in code, for example in an initializer:
Sidekiq::Cron::Job.create(
name: "Nightly billing",
cron: "0 3 * * * Europe/Zurich",
class: "NightlyBillingJob"
)
sidekiq-cron uses Redis to make sure only one process enqueues each scheduled run, so several Sidekiq processes don't create duplicates.
Step 2: A small ping helper
# app/lib/heartbeat.rb
require "net/http"
module Cuckoo
def self.ping(url, suffix = "", body: nil)
return if url.blank?
uri = URI(url + suffix)
Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 10) do |http|
body ? http.post(uri.path, body.byteslice(0, 10_000)) : http.get(uri.path)
end
rescue StandardError => e
Sidekiq.logger.warn("heartbeat ping failed: #{e.message}")
end
end
The helper rescues every network error, so a monitoring problem never turns into a job failure (and a retry).
Step 3: Ping inside perform
class NightlyBillingJob
include Sidekiq::Job
sidekiq_options retry: 3
PING_URL = ENV["CUCKOO_NIGHTLY_BILLING"] # https://cuckoo.metastic.ch/ping/YOUR-UUID
sidekiq_retries_exhausted do |job, exception|
Cuckoo.ping(PING_URL, "/fail", body: "#{exception.class}: #{exception.message}")
end
def perform
Cuckoo.ping(PING_URL, "/start")
Billing.charge_renewals!
Cuckoo.ping(PING_URL)
end
end
Here the fail ping is sent only once Sidekiq has given up. Failed attempts that will be retried don't alert. If
all retries fail, sidekiq_retries_exhausted reports the failure. If the job hangs or is never enqueued, the
success ping is missing and the check goes down when the grace time runs out.
Retries versus fail pings
Choose one of two behaviours:
- Alert only on final failure (shown above). Make sure the whole retry window fits inside the grace time.
Sidekiq's default retry backoff grows quickly, so with
retry: 3the last attempt comes within minutes. With the default of 25 retries it can be weeks later, so lowerretryfor cron jobs. - Alert on every failed attempt. Use
rescueinperformand re-raise, so Sidekiq still retries:
def perform
Cuckoo.ping(PING_URL, "/start")
Billing.charge_renewals!
Cuckoo.ping(PING_URL)
rescue => e
Cuckoo.ping(PING_URL, "/fail", body: "#{e.class}: #{e.message}")
raise
end
When a later retry succeeds, Cuckoo sends a recovery message. Many teams use retry: 0 for idempotent cron
jobs and simply wait for the next scheduled run.
Avoid sending the success ping from an ensure block. ensure also runs when the job raises, so a failure would
be reported as success.
Sidekiq Enterprise periodic jobs
Sidekiq Enterprise has its own periodic jobs feature (config.periodic with mgr.register). The ping code in
perform is the same there, because Cuckoo only cares that the job reports in, not which scheduler enqueued it.
Schedule and grace advice
- Use the job's cron expression and time zone in Cuckoo (
0 3 * * *,Europe/Zurich). - Grace time should cover queue latency plus runtime. A busy
defaultqueue can delay a cron job a lot. Give important cron jobs their own queue. - Set max runtime if a job can hang on an external API. The
/startping lets Cuckoo detect that.
Troubleshooting
- No pings, nothing in Sidekiq: check the Cron tab in Sidekiq Web. If the job is missing, the schedule wasn't
loaded. Check
config/schedule.ymland your initializer. - Enqueued but never run: the queue isn't processed by any Sidekiq process. Compare
queue:with the queues your processes listen on. - Duplicate pings: the job is defined twice under different names, for example once in YAML and once in code.
FAQ
Does the fail ping from sidekiq_retries_exhausted run in the job process?
Yes. Sidekiq calls the block in the process that ran the last attempt, right before it moves the job to the dead set. The same ping helper and environment variables are available there.
Can I monitor that Sidekiq itself is running?
Yes. Add a cron job that runs every five minutes and only pings, and give its check a 5-minute period and grace time. If all Sidekiq processes or Redis stop, that check goes down first.
Should I use the ping URL or the exit-code form?
Inside Ruby, use the success URL and /fail. The exit-code form is meant for shell wrappers, where $? is
available.
Related guides
- Monitor Django and Celery Beat Periodic TasksMonitor Celery beat periodic tasks and Django management commands with start, success and fail pings, and get alerted when beat or the worker stops.
- Monitor GoodJob Cron Jobs with CuckooMonitor GoodJob cron jobs in Rails: configure enable_cron and cron entries, then ping Cuckoo with an ActiveJob around_perform callback on each run.
- Monitor Rails Cron Jobs Built with WheneverAdd heartbeat monitoring to Rails cron jobs from the whenever gem: a custom job_type with curl, or Net::HTTP pings inside rake and runner tasks.
- Monitor the Laravel Scheduler with CuckooMonitor Laravel scheduled tasks with pingBefore, pingOnSuccess and pingOnFailure, plus a check for schedule:run itself. Works on Laravel 10 to 13.