Skip to content

Monitor Rails recurring jobs

Recurring jobs in Rails fail quietly. The scheduler process isn’t running on the new server, the queue backs up behind a slow job, or the job starts raising and exhausts its retries. A heartbeat check catches all of these: the job reports to Pingstack after it succeeds, and Pingstack alerts you when that report doesn’t arrive on time.

This recipe adds a small concern that reports after a successful perform. It works with any Active Job backend, including Solid Queue and Sidekiq.

1. Create a heartbeat check that matches the schedule

Section titled “1. Create a heartbeat check that matches the schedule”

Find the job’s schedule. With Solid Queue, it’s in config/recurring.yml:

production:
nightly_report:
class: NightlyReportJob
schedule: "0 2 * * * UTC"

Solid Queue accepts natural language (every day at 2am) or cron. A cron expression with an explicit timezone, as above, is easiest to match. With sidekiq-cron, the schedule is the job’s cron entry.

Create a heartbeat check and set its schedule to Cron with the same expression and timezone. If your schedule has no timezone, it follows the server’s timezone; choose that in Pingstack. See schedules and timezones.

Set a grace period that covers the job’s runtime, time waiting in the queue and any retries. Attach a notification group.

Copy the location’s ping URL from the check in Pingstack and add it to your encrypted credentials:

Terminal window
bin/rails credentials:edit --environment production
pingstack:
nightly_report: YOUR_PING_URL

Replace YOUR_PING_URL with the URL. Credentials are encrypted, so the URL stays out of plain text in your repository. An environment variable works too.

Add a concern that reports after the job finishes:

app/jobs/concerns/pingstack_heartbeat.rb
require "net/http"
module PingstackHeartbeat
extend ActiveSupport::Concern
class_methods do
def pingstack_heartbeat(credential_key)
after_perform do
url = Rails.application.credentials.dig(:pingstack, credential_key)
PingstackHeartbeat.report(url) if url.present?
end
end
end
def self.report(url, value: nil)
uri = URI(url)
uri.query = URI.encode_www_form(value: value) unless value.nil?
Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 10) do |http|
http.get(uri.request_uri)
end
rescue StandardError => error
Rails.logger.warn("Pingstack heartbeat failed: #{error.class}")
end
end

Then use it in the job:

class NightlyReportJob < ApplicationJob
include PingstackHeartbeat
pingstack_heartbeat :nightly_report
def perform
Reports::Nightly.new.deliver
end
end

after_perform runs only when perform returns without raising, so a failed run sends nothing and Pingstack alerts you when the deadline passes. If the job is retried and later succeeds, it reports then. A failure to reach Pingstack is logged, not raised, so it can’t make a successful job retry.

Run the job once in a production console:

NightlyReportJob.perform_now

Open the check in Pingstack and confirm the location received a heartbeat and shows a next deadline.

Then confirm the scheduler runs in production. With Solid Queue, the dispatcher that enqueues recurring tasks must be running (bin/jobs, or the Puma plugin). With sidekiq-cron, Sidekiq itself loads the schedule.

To send a count as a numeric value, report from the end of perform instead of the concern’s callback:

def perform
sent = Reports::Nightly.new.deliver
PingstackHeartbeat.report(
Rails.application.credentials.dig(:pingstack, :nightly_report),
value: sent
)
end

Set warning and critical thresholds on the check, so a run that succeeds but sends nothing still raises an alert.

  • Queue backlog. A recurring job is enqueued on time but waits behind other work. Lengthen the grace period, or give it its own queue.
  • Retries. A job that succeeds on a later retry reports late.
  • Timezone mismatch. Natural-language schedules without a timezone use the server’s local time.
  • Scheduler not running. Workers are up but nothing enqueues the job. Every recurring check alerts at once, which points at the scheduler.

If alerts still don’t match what you expect, see troubleshooting monitoring.