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.
2. Store the ping URL in credentials
Section titled “2. Store the ping URL in credentials”Copy the location’s ping URL from the check in Pingstack and add it to your encrypted credentials:
bin/rails credentials:edit --environment productionpingstack: nightly_report: YOUR_PING_URLReplace 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.
3. Report after a successful perform
Section titled “3. Report after a successful perform”Add a concern that reports after the job finishes:
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}") endendThen use it in the job:
class NightlyReportJob < ApplicationJob include PingstackHeartbeat pingstack_heartbeat :nightly_report
def perform Reports::Nightly.new.deliver endendafter_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.
4. Confirm the first report
Section titled “4. Confirm the first report”Run the job once in a production console:
NightlyReportJob.perform_nowOpen 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.
Optional: report a number
Section titled “Optional: report a number”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 )endSet warning and critical thresholds on the check, so a run that succeeds but sends nothing still raises an alert.
Why a healthy job might still alert
Section titled “Why a healthy job might still 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.