Cuckoo

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/start when perform begins
  • https://cuckoo.metastic.ch/ping/YOUR-UUID when it finishes
  • https://cuckoo.metastic.ch/ping/YOUR-UUID/fail when 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:

  1. 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: 3 the last attempt comes within minutes. With the default of 25 retries it can be weeks later, so lower retry for cron jobs.
  2. Alert on every failed attempt. Use rescue in perform and 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 default queue 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 /start ping 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.yml and 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

All guides · Compare Cuckoo with other tools

Send feedback

Ideas, bugs, questions: we read every message. Or write to hello@metastic.ch.