tutorial

How to Monitor Good Job with Vigilmon

Good Job is a PostgreSQL-backed ActiveJob backend for Ruby on Rails — but silent worker crashes, queue backlogs, and jobs stuck in the execution table can halt your background processing without a single alert. Learn how to monitor queue health, worker liveness, and job failure rates with Vigilmon.

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

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL: https://your-app.example.com/health/good_job
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 5000ms
  6. Under Alert channels, assign your Slack or email integration
  7. 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

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: good-job-daily-digest
  3. Set the expected interval matching your cron schedule (e.g. 24 hours)
  4. Set the grace period: 30 minutes
  5. Save — copy the heartbeat URL
  6. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →