Claude Code Plugins

Community-maintained marketplace

Feedback

Rails background job patterns for Sidekiq/ActiveJob. Use when creating jobs, handling job errors, or debugging async issues. Covers thin jobs, error strategies, and naming conventions.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rails-jobs
description Rails background job patterns for Sidekiq/ActiveJob. Use when creating jobs, handling job errors, or debugging async issues. Covers thin jobs, error strategies, and naming conventions.

Rails Background Jobs

Patterns for reliable, maintainable background jobs.

Philosophy: Keep Jobs Thin

Jobs handle job-specific concerns (enqueuing, retries, serialization). Business logic lives in domain models.

# Good - job delegates to model
class ProcessOrderJob < ApplicationJob
  def perform(order_id)
    order = Order.find(order_id)
    order.process!  # Business logic in the model
  end
end

# Avoid - business logic in job
class ProcessOrderJob < ApplicationJob
  def perform(order_id)
    order = Order.find(order_id)
    order.update!(status: "processing")
    PaymentService.charge(order.user, order.total)
    order.update!(status: "completed")
    OrderMailer.confirmation(order).deliver_later
  end
end

The _later / _now Convention

Use _later suffix for async methods, _now for synchronous:

module Event::Relaying
  extend ActiveSupport::Concern

  included do
    after_create_commit :relay_later
  end

  def relay_later
    Event::RelayJob.perform_later(self)
  end

  def relay_now
    # Actual relay logic here
    EventService.broadcast(self)
  end
end

class Event::RelayJob < ApplicationJob
  def perform(event)
    event.relay_now
  end
end

This pattern:

  • Makes async behavior explicit at call sites
  • Allows synchronous execution in tests or console
  • Follows Rails conventions (ActionMailer's deliver_later)

Error Handling Strategy

discard_on for Permanent Failures

Use discard_on when retrying won't help:

class UserEnrichmentJob < ApplicationJob
  # Quota exceeded - retrying won't help
  discard_on RestClient::PaymentRequired

  # Record deleted during processing
  discard_on ActiveJob::DeserializationError

  # Invalid credentials
  discard_on RestClient::Unauthorized

  def perform(user_id)
    user = User.find(user_id)
    EnrichmentService.enrich(user)
  end
end

retry_on for Transient Failures

Use retry_on for temporary issues:

class ExternalApiJob < ApplicationJob
  # Service temporarily down
  retry_on RestClient::ServiceUnavailable, wait: 1.minute, attempts: 3

  # Rate limited - wait and retry
  retry_on RestClient::TooManyRequests, wait: :polynomially_longer, attempts: 5

  # Timeout - might work next time
  retry_on Timeout::Error, wait: 30.seconds, attempts: 3

  def perform(resource_id)
    # ...
  end
end

Log When Discarding

Always log for visibility when discarding jobs:

class WebhookProcessingJob < ApplicationJob
  discard_on(ActiveRecord::RecordNotFound) do |job, error|
    Rails.logger.info "Discarding #{job.class}: #{error.message}"
  end

  discard_on(RestClient::PaymentRequired) do |job, error|
    Rails.logger.warn "API quota exceeded for #{job.arguments.first}: #{error.message}"
  end
end

Serialization Gotchas

Arguments Become JSON

ActiveJob serializes arguments to JSON. Watch for:

# Symbols become strings
MyJob.perform_later(status: :active)
# In perform: options["status"] == "active" (string, not symbol)

# Use string keys or convert explicitly
def perform(options)
  status = options["status"].to_sym
end

StringInquirer Issues

Rails environment is a StringInquirer, not a plain string:

# This can fail in Sidekiq
MyJob.perform_later(env: Rails.env)  # StringInquirer doesn't serialize

# Convert to string first
MyJob.perform_later(env: Rails.env.to_s)

Handle Missing Records

Records may be deleted between enqueue and perform:

class NotificationJob < ApplicationJob
  # Option 1: Discard gracefully
  discard_on ActiveJob::DeserializationError

  # Option 2: Handle in perform
  def perform(user_id)
    user = User.find_by(id: user_id)
    return if user.nil?  # Record was deleted

    NotificationService.send(user)
  end
end

Job Organization

Naming Conventions

# Verb + Noun + Job
ProcessOrderJob
SendWelcomeEmailJob
SyncStripeCustomerJob
RefreshMaterializedViewJob

# Namespaced for related jobs
module Scheduled
  class DailyReportJob < ApplicationJob; end
  class WeeklyCleanupJob < ApplicationJob; end
end

Thin Scheduled Jobs

Scheduled jobs should delegate to query objects or services:

class Scheduled::MonitorSidekiqJob < ApplicationJob
  def perform
    stats = SidekiqMonitoring.queue_stats
    Rails.logger.info stats.merge(tags: ["sidekiq", "monitoring"])
  end
end

# Logic lives in module, not job
module SidekiqMonitoring
  def self.queue_stats
    {
      enqueued: Sidekiq::Stats.new.enqueued,
      scheduled: Sidekiq::Stats.new.scheduled_size,
      busy: Sidekiq::ProcessSet.new.total_concurrency
    }
  end
end

Common Patterns

Batch Operations

Move expensive work off the request path:

# Before: N jobs in webhook handler (timeout risk)
users.find_each do |user|
  NotifyUserJob.perform_later(user)
end

# After: Single job, batch in background
NotifyTeamJob.perform_later(team_id: team.id)

class NotifyTeamJob < ApplicationJob
  def perform(team_id:)
    team = Team.find(team_id)
    team.users.find_each do |user|
      NotifyUserJob.perform_now(user)
    end
  end
end

Idempotent Jobs

Design jobs to be safely retried:

class FulfillOrderJob < ApplicationJob
  def perform(order_id)
    order = Order.find(order_id)

    # Guard against duplicate processing
    return if order.fulfilled?

    order.fulfill!
  end
end

Quick Reference

Scenario Strategy
Quota/payment exceeded discard_on
Invalid credentials discard_on
Record deleted discard_on DeserializationError
Service unavailable retry_on with backoff
Rate limited retry_on with polynomial wait
Timeout retry_on with limited attempts
Do Avoid
Keep jobs thin, delegate to models Business logic in jobs
Use _later/_now naming Inconsistent async naming
Log when discarding Silent discards
Convert types before enqueue Assume serialization preserves types
Design for idempotency Assume exactly-once delivery