# How to Use GoodJob for Background Jobs in Rails

[GoodJob](https://github.com/bensheldon/good_job) 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!

[ad-uptime-small]

## 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](https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project) 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](https://www.postgresql.org/download/).

After installing these dependencies, verify PostgreSQL is running:

```command
psql --version
```

A successful installation returns something like:

```text
[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:

```command
rails new goodjob-demo --database=postgresql
```

Navigate to the project directory:

```command
cd goodjob-demo
```

Before creating the database, check your PostgreSQL timezone configuration:

```command
psql postgres -c "SHOW timezone;"
```

```text
[output]
    TimeZone
-----------------
 Africa/Blantyre
(1 row)
```

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

```yaml
[label config/database.yml]
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  [highlight]
  variables:
    timezone: 'Africa/Blantyre'
  [/highlight]

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:

```command
rails db:create
```

```text
[output]
Created database 'goodjob_demo_development'
Created database 'goodjob_demo_test'
```

Add GoodJob to your `Gemfile`:

```ruby
[label Gemfile]
source "https://rubygems.org"

gem "rails", "~> 8.0"
gem "pg", "~> 1.5"
[highlight]
gem "good_job", "~> 4.11"
[/highlight]
# other gems...
```

Install the gem:

```command
bundle install
```

Run GoodJob's installation generator to create the necessary database tables and configuration:

```command
rails generate good_job:install
```

```text
[output]
      create  config/initializers/good_job.rb
      create  db/migrate/20251104100000_create_good_jobs.rb
```

Apply the migration to create GoodJob's tables:

```command
rails db:migrate
```

```text
[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:

```command
rails generate job WelcomeEmail
```

```text
[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:

```ruby
[label app/jobs/welcome_email_job.rb]
class WelcomeEmailJob < ApplicationJob
[highlight]
  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
[/highlight]
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`:

```ruby
[label config/application.rb]
module GoodjobDemo
  class Application < Rails::Application
    config.load_defaults 8.0
    [highlight]
    config.active_job.queue_adapter = :good_job
    [/highlight]
    # other configuration...
  end
end
```

Enqueue a job from the Rails console:

```command
rails console
```

```text
[output]
Loading development environment (Rails 8.0.4)
goodjob-demo(dev)>
```

```ruby
WelcomeEmailJob.perform_later('user@example.com')
```

```text
[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:

```command
bundle exec good_job start
```

```text
[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:

```command
tail -f log/development.log
```

You'll see detailed job execution logs:

```text
[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:

```command
rails generate job DataExport
```

```ruby
[label app/jobs/data_export_job.rb]
class DataExportJob < ApplicationJob
[highlight]
  queue_as :low_priority

  def perform(report_name, format)
    puts "Generating #{report_name} report in #{format} format"
    # Report generation logic would go here
  end
[/highlight]
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:

```command
rails generate job CriticalNotification
```

```ruby
[label app/jobs/critical_notification_job.rb]
class CriticalNotificationJob < ApplicationJob
[highlight]
  queue_as :critical

  def perform(message)
    puts "Sending critical notification: #{message}"
    # Notification delivery logic
  end
[/highlight]
end
```

GoodJob also supports job priorities using integers - lower numbers mean higher priority:

```ruby
[label app/jobs/critical_notification_job.rb]
class CriticalNotificationJob < ApplicationJob
  queue_as :critical

  def perform(message)
    [highlight]
    self.priority = 10  # Higher priority (lower number)
    [/highlight]
    puts "Sending critical notification: #{message}"
  end
end
```

Configure GoodJob to process specific queues with different thread counts. Update `config/initializers/good_job.rb`:

```ruby
[label config/initializers/good_job.rb]
Rails.application.configure do
  [highlight]
  config.good_job.queues = 'critical:2;default:3;low_priority:1'
  config.good_job.max_threads = 6
  [/highlight]
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:

```command
bundle exec good_job start
```

```text
[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:

```command
tail -f log/development.log
```

In a third terminal, open the Rails console and enqueue jobs from different queues:

```command
rails console
```

```ruby
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]`:

```text
[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:

```command
rails generate job Reminder
```

```ruby
[label app/jobs/reminder_job.rb]
class ReminderJob < ApplicationJob
[highlight]
  queue_as :default

  def perform(user_id, message)
    puts "Reminder for user #{user_id}: #{message}"
    # Send reminder notification
  end
[/highlight]
end
```

Open the Rails console to test delayed job execution:

```command
rails console
```

Schedule jobs using `set` with `wait` or `wait_until`:

```ruby
# 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:

```command
tail -f log/development.log
```

```text
[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`:

```ruby
[label 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
  [highlight]
  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'
    }
  }
  [/highlight]
end
```

Generate the backup job:

```command
rails generate job DatabaseBackup
```

```ruby
[label app/jobs/database_backup_job.rb]
class DatabaseBackupJob < ApplicationJob
[highlight]
  queue_as :default

  def perform
    puts "Starting database backup at #{Time.current}"
    # Backup logic goes here
  end
[/highlight]
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:

```ruby
# 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:

```command
bundle exec good_job start
```

```text
[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](https://github.com/bensheldon/good_job) and [Better Stack's monitoring guides](https://betterstack.com/docs/).

Thanks for following along!
