Good Job is a PostgreSQL-native ActiveJob backend for Ruby on Rails. By leaning on pg_notify and advisory locks, it eliminates the Redis dependency that Sidekiq requires while providing reliable at-least-once job execution with robust retry semantics. It's popular in Rails shops already running Postgres who want background jobs without adding another infrastructure component.
But a crashed Good Job process, a job class raising an unhandled exception on every retry, or a growing pile of discarded jobs can leave your application silently broken. Vigilmon gives you external visibility into Good Job health through HTTP monitors for queue health endpoints and heartbeat monitors for worker processes.
Why Good Job Needs External Monitoring
Good Job exposes a built-in web dashboard (the Good Job engine), but it's a pull interface — you have to look at it. External monitoring with Vigilmon adds:
- Proactive alerting when Good Job worker processes go offline
- Queue depth threshold checks before scheduled jobs start piling up
- Discarded job detection — jobs that have exhausted all retries and need developer attention
- Error rate tracking to catch job classes repeatedly failing before the discard threshold
- Heartbeat monitoring for jobs that must prove they are executing on schedule
Step 1: Build a Good Job Health Endpoint
Good Job stores all state in the good_jobs table. Add a controller that queries it:
# app/controllers/health/good_job_controller.rb
module Health
class GoodJobController < ApplicationController
skip_before_action :authenticate_user!, raise: false
QUEUED_THRESHOLD = (ENV['GJ_QUEUED_THRESHOLD'] || 500).to_i
DISCARDED_THRESHOLD = (ENV['GJ_DISCARDED_THRESHOLD'] || 20).to_i
ERROR_THRESHOLD = (ENV['GJ_ERROR_THRESHOLD'] || 100).to_i
def show
issues = []
queued = GoodJob::Job.queued.count
running = GoodJob::Job.running.count
discarded = GoodJob::Job.discarded.count
errored = GoodJob::Job.where.not(error: nil).where(finished_at: nil).count
executions = GoodJob::Execution.where(active_job_id: nil).count rescue 0
issues << "queued_jobs_#{queued}" if queued > QUEUED_THRESHOLD
issues << "discarded_jobs_#{discarded}" if discarded > DISCARDED_THRESHOLD
issues << "errored_jobs_#{errored}" if errored > ERROR_THRESHOLD
issues << "no_running_workers" if running.zero? && queued > 0
stats = {
queued: queued,
running: running,
discarded: discarded,
errored: errored,
}
if issues.any?
render json: { status: 'degraded', issues: issues, stats: stats }, status: :service_unavailable
else
render json: { status: 'ok', stats: stats }
end
end
end
end
Register the route:
# config/routes.rb
Rails.application.routes.draw do
namespace :health do
resource :good_job, only: :show
end
# ...
end
Test locally:
curl http://localhost:3000/health/good_job
# {"status":"ok","stats":{"queued":0,"running":2,"discarded":0,"errored":0}}
Step 2: Monitor Stale Running Jobs
Jobs stuck in the running state without progressing indicate a worker crash that didn't release the advisory lock. Postgres advisory locks are released on connection close, but zombie records can remain if the process was killed hard:
# app/controllers/health/good_job_controller.rb (add stale action)
def stale
stale_threshold = (ENV['GJ_STALE_MINUTES'] || 15).to_i.minutes.ago
stale_count = GoodJob::Job.running
.where('performed_at < ?', stale_threshold)
.count
if stale_count > 0
render json: { status: 'degraded', stale_running: stale_count }, status: :service_unavailable
else
render json: { status: 'ok', stale_running: 0 }
end
end
Add the route:
namespace :health do
resource :good_job, only: :show do
get :stale, on: :collection
end
end
Step 3: Add Heartbeat Pings to Critical Jobs
HTTP queue checks confirm the queue table is healthy. Heartbeats confirm specific jobs are actually executing:
# app/jobs/invoice_generation_job.rb
class InvoiceGenerationJob < ApplicationJob
queue_as :billing
retry_on StandardError, wait: :polynomially_longer, attempts: 5
def perform(subscription_id)
generate_invoice(subscription_id)
# Ping heartbeat only on success
if (url = ENV['VIGILMON_INVOICE_JOB_HEARTBEAT'])
Net::HTTP.get(URI(url)) rescue nil
end
end
private
def generate_invoice(subscription_id)
# your logic
end
end
For recurring jobs scheduled with the good_job cron configuration:
# config/initializers/good_job.rb
GoodJob.configure do |config|
config.execution_mode = :external
config.cron = {
daily_digest: {
cron: '0 8 * * *',
class: 'DailyDigestJob',
},
hourly_sync: {
cron: '0 * * * *',
class: 'HourlySyncJob',
},
}
end
Each scheduled job class should ping its own heartbeat URL on completion:
class DailyDigestJob < ApplicationJob
queue_as :default
def perform
send_digest_emails
Net::HTTP.get(URI(ENV['VIGILMON_DAILY_DIGEST_HEARTBEAT'])) rescue nil
end
end
Step 4: Configure Vigilmon HTTP Monitors
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL:
https://your-app.example.com/health/good_job - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or email integration
- Save the monitor
Add monitors for each concern:
| Monitor URL | Purpose | Interval |
|---|---|---|
| /health/good_job | Queue depth, discarded count, running workers | 1 min |
| /health/good_job/stale | Stale running job detection | 2 min |
Step 5: Configure Heartbeat Monitors for Workers
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
good-job-daily-digest - Set the expected interval matching your cron schedule (e.g. 24 hours)
- Set the grace period: 30 minutes
- Save — copy the heartbeat URL
- Set the corresponding environment variable in production
Create one heartbeat monitor per critical job:
| Job Class | Heartbeat Interval | Grace Period | |---|---|---| | DailyDigestJob | 24 hours | 30 min | | HourlySyncJob | 1 hour | 10 min | | InvoiceGenerationJob | 5 min | 10 min |
Step 6: Alert Routing
| Monitor | Alert Channel | Priority |
|---|---|---|
| Queue health /health/good_job | Slack + PagerDuty | P1 |
| Stale jobs /health/good_job/stale | Slack | P2 |
| Worker heartbeats | Slack + PagerDuty | P1 |
Key thresholds to tune for your workload:
- Queued threshold: 2–3× your normal peak — alert when jobs are accumulating faster than workers clear them
- Discarded threshold: keep low — discarded jobs have exhausted all retries and represent unrecoverable failures
- Stale window: 15 minutes is a safe default; increase for legitimately long-running jobs
- Error threshold: alert when recurring errors suggest a systemic job class bug rather than transient failures
Summary
Good Job failures are database-silent — no external process crashes loudly and no Redis alarm fires. External monitoring with Vigilmon catches problems before they become incidents:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /health/good_job | Queue depth, discarded count, running worker presence |
| HTTP monitor on /health/good_job/stale | Stale running job detection |
| Heartbeat monitor | Specific job class execution on schedule |
Get started free at vigilmon.online — your first Good Job monitor is running in under two minutes.