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:migrateconfig/application.rb
config.active_job.queue_adapter = :pgbus # was :good_jobConcurrency 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}" }
endSee 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
_dlqqueue 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_timeoutcan be re-read, so size the timeout above your longest job (or extend it in-job). - Table cleanup — after jobs drain, drop the
good_jobstables; pgbus doesn't read them.