Back to blog

Fire and Forget for Enhanced Performance

January 21, 20243 min read

The Fire and Forget technique is a powerful method to enhance API performance by allowing tasks to proceed without waiting for a response. This approach is particularly beneficial in scenarios like Service Bus updates during user login, where immediate feedback is not required, thus improving overall system efficiency.

Development Series — 23 articles
  1. Mastering Git Repository Organization
  2. CancellationToken for Async Programming
  3. Git Flow Rethink: When Process Stops Paying Rent
  4. Understanding System Cache: A Comprehensive Guide
  5. Guide to Redis Local Instance Setup
  6. Fire and Forget for Enhanced Performance
  7. Building Resilient .NET Applications with Polly
  8. The Singleton Advantage: Managing Configurations in .NET
  9. Troubleshooting and Rebuilding My JS-Dev-Env Project
  10. Decorator Design Pattern - Adding Telemetry to HttpClient
  11. Generate Wiki Documentation from Your Code Repository
  12. TaskListProcessor - Enterprise Async Orchestration for .NET
  13. Architecting Agentic Services in .NET 9: Semantic Kernel
  14. NuGet Packages: Benefits and Challenges
  15. My Journey as a NuGet Gallery Developer and Educator
  16. Harnessing the Power of Caching in ASP.NET
  17. The Building of React-native-web-start
  18. TailwindSpark: Ignite Your Web Development
  19. Creating a PHP Website with ChatGPT
  20. Evolving PHP Development
  21. Modernizing Client Libraries in a .NET 4.8 Framework Application
  22. Building Git Spark: My First npm Package Journey
  23. Dave's Top Ten: Git Stats You Should Never Track

Fire and Forget for Enhanced Performance

Understanding the Fire and Forget Technique

I was debugging why user logins were timing out on a high-traffic API when I traced the problem to a single culprit: we were synchronously publishing a login event to a service bus before returning the auth response. The bus was under load, latency spiked, and users felt every millisecond of it. We couldn't fix the bus latency in time for the next release—so we decoupled it. That's where Fire and Forget came in.

The pattern is straightforward in concept: dispatch a task and move on without waiting for it to complete. But in my experience, the simplicity of the concept masks how easy it is to misuse in production.

Benefits of Fire and Forget

  • Improved Performance: By not waiting for a response, systems can handle more requests in a shorter time frame, leading to better overall performance.
  • Reduced Latency: Eliminating the need to wait for a response reduces the time taken to complete tasks, especially in high-load environments.
  • Resource Efficiency: Systems can allocate resources more effectively by not holding them up for responses that are not critical.

Application in API Performance

One of the primary applications of Fire and Forget is in keeping the critical request path lean. When a user logs in, there's often a cluster of background work—audit logging, metrics, service bus updates—that doesn't need to block the auth response. The user doesn't care whether your telemetry pipeline confirmed receipt. They care about getting their token and moving on.

What I've found, though, is that "doesn't need to block" and "doesn't need to succeed" are two very different things. That distinction is where most implementations go wrong.

Example: Service Bus Updates

On the login latency project, we moved the service bus publish into a Fire and Forget task. Here's a more realistic version of what that looked like—not a simulation, but a pattern close to production use:

public void UpdateServiceBus(string userId)
{
    // Fire and Forget: do not await this task
    _ = Task.Run(async () =>
    {
        try
        {
            await _serviceBusClient.PublishLoginEventAsync(userId);
        }
        catch (Exception ex)
        {
            // Without this catch, unobserved exceptions get swallowed silently
            _logger.LogError(ex, "Fire-and-forget service bus publish failed for user {UserId}", userId);
        }
    });
}

Even this isn't bulletproof. The _ = discard tells the compiler you're intentionally not awaiting, but the task is still subject to app shutdown. If the application pool recycles while that task is in flight, the publish never happens—and nothing tells you. On a long-running service this is manageable, but in Azure App Service with aggressive recycling, I've watched Fire and Forget tasks vanish silently under load. The right mitigation depends on the stakes: for low-value audit pings, the loss is acceptable; for anything that affects downstream state, you need a more durable pattern like a background queue or hosted service.

The other gotcha is unobserved exceptions. Before .NET 4.5, an unobserved task exception would crash the process. That behavior changed, but unobserved exceptions still disappear without a trace unless you explicitly catch them inside the task body or hook TaskScheduler.UnobservedTaskException. I've seen both failure modes in production—silent metric loss and, on older codebases, outright crashes.

Considerations

Here's what I've learned the hard way about Fire and Forget:

  • Error Handling: You can't use standard exception handling in the main request path to catch failures inside a Fire and Forget task—by the time the task throws, your caller has already moved on. In practice, I wrap the entire task body in a try/catch and log explicitly. If I skip that, the failure is completely invisible. I've had metrics pipelines go dark for hours because a serialization exception inside a fire-and-forget call was silently swallowed.
  • Task Completion: The system doesn't wait for a response, but that doesn't mean you shouldn't care whether the work completes. For anything beyond truly throwaway telemetry, I add a counter or a health check so I can tell if the background work is succeeding at a reasonable rate. Drift in those numbers is often the first signal that something is wrong.

The trade-off here is real: you're buying latency on the critical path by accepting reduced observability and reliability for the background work. That's a reasonable trade for audit logs or non-critical notifications. It's a bad trade for anything that affects user-facing state or downstream consistency.

Conclusion

Fire and Forget is a tool I reach for when latency on the critical path is choking performance and the background work truly doesn't need a synchronous response. But I've also shipped code where I used it carelessly and regretted it—silent failures, lost events, tasks killed mid-flight during deployments. It's a tradeoff, not a free win. The pattern works well when you understand exactly what you're giving up, and it bites you when you treat it as a simple performance shortcut.

Explore More