Skip to Content
Course content

87: Active Job for Background Processing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Let's look at a common bottleneck I run into in almost every Rails app: the "hanging" request. Imagine we're building a platform where users sign up, and the moment they do, we send them a welcome email. It seems simple enough, right?

The Three-Second Freeze

I've written a basic controller action to handle this. I'm calling the mailer directly inside the create action. Let's see what happens when I hit the "Sign Up" button in my browser.

def create
  @user = User.new(user_params)
  if @user.save
    # Sending the email right here in the request cycle
    UserMailer.welcome_email(@user).deliver_now
    redirect_to @user, notice: "Welcome aboard!"
  else
    render :new
  end
end

When I click submit, the browser spinner just... rotates. For about two or three seconds, nothing happens. The page doesn't load, and the user is left wondering if they clicked the button or if the site crashed. The reason is that deliver_now is a synchronous call. My Ruby process is literally sitting there, waiting for the external SMTP server to acknowledge that the email was sent before it can move on to the redirect_to line.

That's a terrible user experience. The user doesn't care if the email is sent at 10:00:00 AM or 10:00:02 AM; they just want to see their profile page immediately.

Moving the Work Off-Thread

This is where Active Job comes in. Instead of doing the work now, I want to describe the work and tell Rails to do it later. First, I'll generate a job class to handle this specific task.

bin/rails generate job SendWelcomeEmail

This gives me a file in app/jobs/send_welcome_email_job.rb. I'll move the mailing logic into the perform method. This is the core of the job; whatever logic goes in here is what will be executed in the background.

class SendWelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user)
    UserMailer.welcome_email(user).deliver_now
  end
end

Now, I need to change my controller. Instead of calling the mailer directly, I'll tell the job to run later. I'm using perform_later here, which is the magic method that pushes the job onto a queue.

def create
  @user = User.new(user_params)
  if @user.save
    # We've swapped deliver_now for a background job
    SendWelcomeEmailJob.perform_later(@user)
    redirect_to @user, notice: "Welcome aboard!"
  else
    render :new
  end
end

I'll try the sign-up again. Boom. The redirect happens instantly. The user is happy. If I check my logs, I can see that the job was enqueued and then processed a split second later. The heavy lifting happened outside the request/response cycle.

Wait, Where Did the Job Actually Go?

Here is the part that trips people up: Active Job is a framework, not a queue. It's a standardized wrapper. It provides the perform_later syntax, but it doesn't actually store the jobs itself. It needs a "Queue Adapter" to do the heavy lifting.

By default, in a new Rails app, the adapter is set to :async. This is an in-memory queue that works great for development, but it has a huge flaw: if you restart your server, every job currently waiting in the queue is deleted. That's fine for a welcome email in dev, but it's a disaster for processing a $1,000 payment in production.

If I want a persistent queue—something that survives a crash—I'd use something like Sidekiq (which uses Redis) or Solid Queue (which uses your database). Switching them is as simple as changing a line in config/application.rb:

config.active_job.queue_adapter = :sidekiq

One quick tip: you'll notice I passed the @user object into perform_later. Active Job is smart enough to use GlobalID. It doesn't serialize the entire Ruby object (which would be huge and potentially outdated); it just saves the class name and the ID (e.g., "User:123"). When the job actually starts running, it fetches the fresh record from the database using that ID. This is why you should always pass Active Record objects rather than raw hashes when working with jobs.




📋 Practical Task

Implementing a Heavy CSV Export Job

You are building a reporting tool for an admin dashboard. Currently, the "Export Users to CSV" feature is written as a standard controller action that generates a file and sends it. However, as the user base grows, the request is timing out because the CSV generation takes too long.

Your Task:

  • Generate a new Active Job called ExportUsersCsvJob.
  • In the perform method of the job, simulate a heavy process by adding sleep(5), then print a message to the console saying "CSV Export Complete for [Admin Name]".
  • Modify a hypothetical Admin::ReportsController#export action to call this job using perform_later, passing in the current admin user.
  • Ensure the controller action redirects the admin back to the reports index with a flash message saying "Your export has been queued and will be ready shortly."
Rating
0 0

There are no comments for now.

to be the first to leave a comment.