Back to Scaling Ruby Applications guides

How to Use GoodJob for Background Jobs in Rails

Stanley Ulili
Updated on November 5, 2025

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:

Output
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;"
Output
    TimeZone
-----------------
 Africa/Blantyre
(1 row)

Update config/database.yml to match your PostgreSQL timezone:

config/database.yml
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
Output
Created database 'goodjob_demo_development'
Created database 'goodjob_demo_test'

Add GoodJob to your Gemfile:

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
Output
      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
Output

== 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
Output
      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:

app/jobs/welcome_email_job.rb
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:

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
Output
Loading development environment (Rails 8.0.4)
goodjob-demo(dev)>
 
WelcomeEmailJob.perform_later('user@example.com')
Output
  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
Output
[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:

Output
[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
app/jobs/data_export_job.rb
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
app/jobs/critical_notification_job.rb
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:

app/jobs/critical_notification_job.rb
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:

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
Output
[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]:

Output
[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
app/jobs/reminder_job.rb
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
Output
[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:

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
app/jobs/database_backup_job.rb
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
Output
[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!

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.