GoodJob is a multithreaded, Postgres-based background job processor for Ruby that runs as a Rails engine. It excels at handling asynchronous work like sending emails, processing uploads, generating reports, and calling external APIs without slowing down web requests.
What sets GoodJob apart from other job processors is its simplicity - it uses your existing Postgres database instead of requiring Redis or another data store. This reduces infrastructure complexity while providing features like job prioritization, scheduled execution, and built-in monitoring through a web dashboard. Companies like Honeybadger and CodeTriage rely on GoodJob to process background jobs in production.
This tutorial walks you through setting up GoodJob, creating workers, and implementing reliable background processing in a Rails application.
Let's get started!
Prerequisites
You'll need Ruby 3.0 or later and Rails 6.1 or later installed on your system. Check the official Rails installation guide if you haven't set up Rails yet. GoodJob requires PostgreSQL as your database since it stores jobs in Postgres tables. You can find installation instructions in the PostgreSQL documentation.
After installing these dependencies, verify PostgreSQL is running:
psql --version
A successful installation returns something like:
psql (PostgreSQL) 18.0 (Homebrew)
Step 1 — Creating a new Rails application
Start by creating a new Rails application with PostgreSQL as the database:
rails new goodjob-demo --database=postgresql
Navigate to the project directory:
cd goodjob-demo
Before creating the database, check your PostgreSQL timezone configuration:
psql postgres -c "SHOW timezone;"
TimeZone
-----------------
Africa/Blantyre
(1 row)
Update config/database.yml to match your PostgreSQL timezone:
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
variables:
timezone: 'Africa/Blantyre'
development:
<<: *default
database: goodjob_demo_development
test:
<<: *default
database: goodjob_demo_test
production:
<<: *default
database: goodjob_demo_production
username: goodjob_demo
password: <%= ENV["GOODJOB_DEMO_DATABASE_PASSWORD"] %>
Replace Africa/Blantyre with the timezone returned by your PostgreSQL installation. Now create and set up the database:
rails db:create
Created database 'goodjob_demo_development'
Created database 'goodjob_demo_test'
Add GoodJob to your Gemfile:
source "https://rubygems.org"
gem "rails", "~> 8.0"
gem "pg", "~> 1.5"
gem "good_job", "~> 4.11"
# other gems...
Install the gem:
bundle install
Run GoodJob's installation generator to create the necessary database tables and configuration:
rails generate good_job:install
create config/initializers/good_job.rb
create db/migrate/20251104100000_create_good_jobs.rb
Apply the migration to create GoodJob's tables:
rails db:migrate
== 20251104083630 CreateGoodJobs: migrating ===================================
-- create_table(:good_jobs, {id: :uuid})
-> 0.0102s
-- create_table(:good_job_batches, {id: :uuid})
-> 0.0049s
-- create_table(:good_job_executions, {id: :uuid})
-> 0.0040s
-- create_table(:good_job_processes, {id: :uuid})
-> 0.0050s
-- create_table(:good_job_settings, {id: :uuid})
-> 0.0086s
GoodJob is now installed and ready to process background jobs.
Step 2 — Creating your first background job
Rails uses Active Job as its interface for background jobs. GoodJob works seamlessly with Active Job, so you'll create jobs using Rails conventions.
Generate a job to send welcome emails:
rails generate job WelcomeEmail
invoke test_unit
create test/jobs/welcome_email_job_test.rb
create app/jobs/welcome_email_job.rb
Open the generated job file and add your job logic:
class WelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user_email)
puts "Sending welcome email to #{user_email}"
# In production, this would contain your email delivery logic
# UserMailer.welcome(user_email).deliver_now
end
end
The queue_as method specifies which queue this job belongs to, and the perform method contains the actual work. Arguments passed to perform when enqueuing the job are available as method parameters.
Configure GoodJob as your Active Job queue adapter by updating config/application.rb:
module GoodjobDemo
class Application < Rails::Application
config.load_defaults 8.0
config.active_job.queue_adapter = :good_job
# other configuration...
end
end
Enqueue a job from the Rails console:
rails console
Loading development environment (Rails 8.0.4)
goodjob-demo(dev)>
WelcomeEmailJob.perform_later('user@example.com')
TRANSACTION (0.2ms) BEGIN
GoodJob::Job Create (15.4ms) INSERT INTO "good_jobs" ("id", "queue_name", "priority"...
TRANSACTION (2.0ms) COMMIT
Enqueued WelcomeEmailJob (Job ID: 9c863976-1e17-4029-8517-ca863c100a49) to GoodJob(default) with arguments: "user@example.com"
=>
#<WelcomeEmailJob:0x000000013c976cc8
@arguments=["user@example.com"],
@job_id="9c863976-1e17-4029-8517-ca863c100a49",
@queue_name="default",
@successfully_enqueued=true>
The job is stored in your Postgres database, waiting for a worker to process it. Start the GoodJob worker in a separate terminal:
bundle exec good_job start
[GoodJob] [38892] GoodJob 4.12.1 started scheduler with queues=* max_threads=5.
[GoodJob] Notifier subscribed with LISTEN
[GoodJob] [38892] [GoodJob::Scheduler(queues=* max_threads=5)-thread-3] Executed GoodJob 9c863976-1e17-4029-8517-ca863c100a49
The worker picked up the job from the database and executed it successfully. The Executed GoodJob line confirms the job ran. To see the detailed output including your puts statements, check the Rails development log in another terminal:
tail -f log/development.log
You'll see detailed job execution logs:
[ActiveJob] [WelcomeEmailJob] [9c863976-1e17-4029-8517-ca863c100a49] Performing WelcomeEmailJob (Job ID: 9c863976-1e17-4029-8517-ca863c100a49) from GoodJob(default) enqueued at 2025-11-04T08:52:51Z with arguments: "user@example.com"
Sending welcome email to user@example.com
[ActiveJob] [WelcomeEmailJob] [9c863976-1e17-4029-8517-ca863c100a49] Performed WelcomeEmailJob (Job ID: 9c863976-1e17-4029-8517-ca863c100a49) from GoodJob(default) in 37.24ms
Jobs now run in the background, separate from your web requests. The log file captures all job output, making it easy to debug and monitor job execution.
Step 3 — Working with multiple queues and priorities
Applications often need different job types with varying urgency levels. GoodJob lets you organize jobs into named queues and assign priorities.
Generate a job for data exports:
rails generate job DataExport
class DataExportJob < ApplicationJob
queue_as :low_priority
def perform(report_name, format)
puts "Generating #{report_name} report in #{format} format"
# Report generation logic would go here
end
end
This job uses the low_priority queue instead of default, allowing you to control how workers allocate resources across different job types.
Generate a high-priority notification job:
rails generate job CriticalNotification
class CriticalNotificationJob < ApplicationJob
queue_as :critical
def perform(message)
puts "Sending critical notification: #{message}"
# Notification delivery logic
end
end
GoodJob also supports job priorities using integers - lower numbers mean higher priority:
class CriticalNotificationJob < ApplicationJob
queue_as :critical
def perform(message)
self.priority = 10 # Higher priority (lower number)
puts "Sending critical notification: #{message}"
end
end
Configure GoodJob to process specific queues with different thread counts. Update config/initializers/good_job.rb:
Rails.application.configure do
config.good_job.queues = 'critical:2;default:3;low_priority:1'
config.good_job.max_threads = 6
end
This configuration dedicates 2 threads to the critical queue, 3 to default, and 1 to low_priority. The max_threads value should equal the sum of all queue thread counts.
Restart the GoodJob worker to apply the new configuration. Stop the current worker with Ctrl+C, then start it again:
bundle exec good_job start
[GoodJob] [42397] GoodJob 4.12.1 started scheduler with queues=critical max_threads=2.
[GoodJob] [42397] GoodJob 4.12.1 started scheduler with queues=default max_threads=3.
[GoodJob] [42397] GoodJob 4.12.1 started scheduler with queues=low_priority max_threads=1.
[GoodJob] Notifier subscribed with LISTEN
Notice that GoodJob now shows three separate schedulers, one for each queue with its configured thread count. Leave this terminal running.
In a new terminal, start monitoring the development log to see job execution in real-time:
tail -f log/development.log
In a third terminal, open the Rails console and enqueue jobs from different queues:
rails console
CriticalNotificationJob.perform_later('System maintenance in 5 minutes')
DataExportJob.perform_later('sales_report', 'pdf')
WelcomeEmailJob.perform_later('new@user.com')
Switch to the terminal monitoring the log file. You'll see the jobs execute with lines containing [ActiveJob]:
[ActiveJob] [CriticalNotificationJob] [abc-123] Performing CriticalNotificationJob from GoodJob(critical)
[ActiveJob] [CriticalNotificationJob] [abc-123] Performed CriticalNotificationJob from GoodJob(critical) in 2.1ms
[ActiveJob] [WelcomeEmailJob] [def-456] Performing WelcomeEmailJob from GoodJob(default)
[ActiveJob] [WelcomeEmailJob] [def-456] Performed WelcomeEmailJob from GoodJob(default) in 1.8ms
[ActiveJob] [DataExportJob] [ghi-789] Performing DataExportJob from GoodJob(low_priority)
[ActiveJob] [DataExportJob] [ghi-789] Performed DataExportJob from GoodJob(low_priority) in 3.4ms
The critical notification processes first with its dedicated threads, followed by the default queue job, and finally the low-priority export task. This demonstrates how queue configuration controls job processing order and resource allocation.
Step 4 — Scheduling jobs for future execution
Beyond immediate processing, GoodJob handles delayed execution and recurring jobs. Active Job provides methods for scheduling jobs to run at specific times.
Create a reminder job:
rails generate job Reminder
class ReminderJob < ApplicationJob
queue_as :default
def perform(user_id, message)
puts "Reminder for user #{user_id}: #{message}"
# Send reminder notification
end
end
Open the Rails console to test delayed job execution:
rails console
Schedule jobs using set with wait or wait_until:
# Execute 30 seconds from now
ReminderJob.set(wait: 30.seconds).perform_later(123, 'Meeting starts soon')
# Execute at a specific time
ReminderJob.set(wait_until: 1.hour.from_now).perform_later(456, 'Review quarterly report')
Exit the console and check your development log to see the job execute after 30 seconds:
tail -f log/development.log
[ActiveJob] [ReminderJob] [abc-123] Performing ReminderJob from GoodJob(default)
Reminder for user 123: Meeting starts soon
[ActiveJob] [ReminderJob] [abc-123] Performed ReminderJob from GoodJob(default) in 1.5ms
For recurring jobs, GoodJob includes a cron-style scheduler. Update config/initializers/good_job.rb:
Rails.application.configure do
config.good_job.queues = 'critical:2;default:3;low_priority:1'
config.good_job.max_threads = 6
config.good_job.enable_cron = true
config.good_job.cron = {
daily_backup: {
cron: '0 2 * * *', # Every day at 2 AM
class: 'DatabaseBackupJob'
},
hourly_cleanup: {
cron: '0 * * * *', # Every hour
class: 'CleanupJob'
}
}
end
Generate the backup job:
rails generate job DatabaseBackup
class DatabaseBackupJob < ApplicationJob
queue_as :default
def perform
puts "Starting database backup at #{Time.current}"
# Backup logic goes here
end
end
The cron expression 0 2 * * * follows the standard cron format: minute, hour, day of month, month, day of week.
Here are more cron expression examples:
# Every 15 minutes
cron: '*/15 * * * *'
# Every Monday at 9 AM
cron: '0 9 * * 1'
# First day of every month at midnight
cron: '0 0 1 * *'
# Every 6 hours
cron: '0 */6 * * *'
Stop the GoodJob worker with Ctrl+C and restart it to activate the scheduled jobs:
bundle exec good_job start
[GoodJob] [61234] GoodJob 4.12.1 started scheduler with queues=critical max_threads=2.
[GoodJob] [61234] GoodJob 4.12.1 started scheduler with queues=default max_threads=3.
[GoodJob] [61234] GoodJob 4.12.1 started scheduler with queues=low_priority max_threads=1.
[GoodJob] [61234] GoodJob 4.12.1 started cron with cron=daily_backup,hourly_cleanup.
[GoodJob] Notifier subscribed with LISTEN
Notice the line showing started cron with cron=daily_backup,hourly_cleanup, confirming that GoodJob registered your recurring jobs. The backup job now runs automatically according to its schedule, with no additional intervention required.
Final thoughts
You've now implemented background job processing in Ruby using GoodJob, covering everything from basic workers to production-ready monitoring and deployment.
GoodJob's Postgres-based approach simplifies infrastructure by eliminating the need for Redis or other external job stores, while still providing features like priorities, scheduled execution, and robust retry handling. The techniques covered here - queue configuration, cron scheduling, batched processing, and operational monitoring - apply to Rails applications of any scale.
For deeper exploration, check out the GoodJob documentation and Better Stack's monitoring guides.
Thanks for following along!