Skip to content

Case 20: Don't Make the User Wait - Offloading Work to Background Jobs

The Story

Imagine an e-commerce website. The user registration process is very slow. After a user fills out the form and clicks "Register," they have to wait 5-10 seconds to see the welcome screen.

The registration process includes:

  1. Validating the data.
  2. Creating a user record in the database. (Fast)
  3. Sending a welcome email. (Slow, because it has to wait for the mail server)
  4. Generating a thumbnail for the profile picture. (Slow, because it involves file processing)
  5. Calling an analytics service's API. (Slow, because it has to wait for the network)

The Problem Code (Doing Everything in the Request)

csharp
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterModel model)
{
    // ... validate model ...

    // 1. Create user in DB (fast)
    var user = await _userService.CreateUserAsync(model);

    // 2. Send email (slow)
    await _emailService.SendWelcomeEmailAsync(user.Email);

    // 3. Generate thumbnail (very slow)
    await _imageService.GenerateAvatarThumbnailAsync(user.Id);

    // 4. Call analytics (slow)
    await _analyticsService.TrackNewUserAsync(user.Id);

    // The user only gets this response after 5-10 seconds
    return Ok("Registration successful!");
}

Why is this a problem?

  1. Making the User Wait: The user's HTTP request is held open while all the slow I/O operations (sending email, processing images, calling APIs) are running. This is a terrible user experience.
  2. Poor Reliability: If the email service fails, the entire registration process fails and shows an error to the user, even though the most important part—creating the account—was successful.
  3. Holding Server Resources: The request-handling thread is occupied for a long time, reducing the server's ability to serve other requests.

The Solution: Offload Work to a Background Job

The only thing the user really needs to wait for is their account to be created. Everything else can be done after the response has been sent back to the user.

How it works:

  1. The controller performs only the core, fast task (creating the user).
  2. Then, it "enqueues" the slow tasks into a queue.
  3. It returns a success response to the user immediately.
  4. A background "worker" automatically picks up jobs from the queue and processes them.

Popular Libraries: Hangfire, Quartz.NET, or the built-in IHostedService in ASP.NET Core.

Here is the better code (example with Hangfire):

csharp
public class RegistrationController : ControllerBase
{
    // ... inject IBackgroundJobClient ...

    [HttpPost("register")]
    public async Task<IActionResult> Register(RegisterModel model)
    {
        // ... validate model ...

        // 1. Create user in DB (the core, fast task)
        var user = await _userService.CreateUserAsync(model);

        // 2. Enqueue the slow tasks.
        // These commands only take a few milliseconds to execute.
        _backgroundJobClient.Enqueue(() => _emailService.SendWelcomeEmailAsync(user.Email));
        _backgroundJobClient.Enqueue(() => _imageService.GenerateAvatarThumbnailAsync(user.Id));
        _backgroundJobClient.Enqueue(() => _analyticsService.TrackNewUserAsync(user.Id));

        // 3. RETURN THE RESPONSE IMMEDIATELY
        return Ok("Registration successful! Please check your email.");
    }
}

What We Gained

  1. Instant Perceived Response Time: The user gets a response in just a few hundred milliseconds, instead of waiting 10 seconds. The application feels incredibly fast.
  2. Increased Reliability and Resilience: If the email service fails, it doesn't affect the user's request. The job in the queue can be configured to automatically retry later.
  3. Better Resource Management: The web server's threads are freed up immediately to serve other users. The heavy work is handled by separate workers, which can even be scaled independently.

Final Words for the Series

Our journey has taken us from optimizing a single SQL query, to the schema, to the database, and finally to the application's architecture. The general principles are always:

  1. Make it work first.
  2. Measure to find the real bottleneck.
  3. Apply the right optimization technique for the right problem.

Thank you for following along with this series. Your curiosity and questions show that you have a great mindset for becoming an excellent software engineer. Good luck!