Migrate

From GoodJob

Both are PostgreSQL-native with LISTEN/NOTIFY — swap advisory locks and good_jobs for PGMQ.

What changes#

GoodJob and pgbus are both PostgreSQL-native with LISTEN/NOTIFY. The architectural difference: GoodJob uses advisory locks and a good_jobs table; pgbus uses PGMQ — a dedicated message-queue extension — with visibility timeouts. Both are pure ActiveJob adapters, so jobs move over unchanged.

Effort: low for standard ActiveJob; medium if you rely on GoodJob's concurrency controls, batches, or cron.

Swap the gem and adapter#

Gemfile
# Remove
gem "good_job"

# Add
gem "pgbus"
bundle install && rails generate pgbus:install && rails db:migrate
config/application.rb
config.active_job.queue_adapter = :pgbus # was :good_job

Concurrency and cron#

GoodJob's good_job_control_concurrency_with maps to pgbus's limits_concurrency; GoodJob's config.good_job.cron maps to pgbus's recurring tasks. Both DSLs are auto-included — no explicit require:

# GoodJob: good_job_control_concurrency_with(total_limit: 1, key: -> { ... })
# pgbus:
class ProcessOrderJob < ApplicationJob
  limits_concurrency to: 1, key: ->(order_id) { "ProcessOrder-#{order_id}" }
end
See Concurrency & uniqueness and Recurring tasks for the full APIs; Batches replaces GoodJob::Batch.

What you gain#

  • Dead-letter queues — GoodJob retries in place; pgbus routes exhausted jobs to a _dlq queue for inspection.
  • Worker recycling — memory, job-count, and lifetime limits.
  • An event bus and Postgres-SSE streams — on the same database.

Gotchas#

  • Locking model — GoodJob's advisory locks are held for the duration of a job; PGMQ uses a visibility timeout that expires and re-delivers. A job that outlives its visibility_timeout can be re-read, so size the timeout above your longest job (or extend it in-job).
  • Table cleanup — after jobs drain, drop the good_jobs tables; pgbus doesn't read them.