The FIFO Fallacy: Why Ordered Queues are Killing Your Scalability

The FIFO Fallacy: Why Ordered Queues are Killing Your Scalability
by Brad Jolicoeur
01/31/2026

When engineers first dip their toes into distributed messaging, one requirement almost always floats to the top of the list: "We need a FIFO queue."

It feels natural. We live in a linear world. We buy groceries in a line. We read books from page 1 to page 300. In our software, it seems obvious that we must process OrderCreated before OrderShipped. If we don't, chaos ensues, right?

This "Grocery Store" mental model is comforting, but in the world of distributed systems, it is a trap. The pursuit of strict Global FIFO (First-In, First-Out) is often the single biggest bottleneck preventing your system from scaling and achieving high availability.

Let's explore why we intuitively crave order, the technical price we pay for it, and how we can build robust, scalable systems that embrace the chaos using patterns like Idempotency and Sagas.

The Intuitive Trap

Why do we default to strict ordering?

  1. Narrative Bias: Humans think in stories. We expect cause and effect to be sequential. We struggle to reason about a world where an effect (Shipping) is observed before the cause (Ordering).
  2. Database Legacy: Many of us "grew up" on monolithic SQL databases. Transaction logs (like SQL Server LSNs) provided a comforting, rigid guarantee of sequence. We try to port this ACID-compliant rigidity into our distributed architecture.
  3. Fear of Complexity: Handling out-of-order messages requires explicit business logic. It's easier to say "the infrastructure will handle it" than to write code that checks "Did I miss a step?"

But distributed infrastructure is not a database, and treating it like one leads to pain.

The High Cost of Strict Order

When you enforce strict FIFO, you are effectively declaring war on parallelism.

1. The Scalability Ceiling (Single Consumer)

To guarantee that Message A finishes before Message B starts, you can effectively only have one active consumer. If you spin up 10 consumers to handle a load spike, nine of them are useless for a FIFO queue. If Consumer 1 picks up Message A, Consumer 2 cannot pick up Message B, because Consumer 2 might be faster. Result: You cannot scale out. Your throughput is capped by the processing speed of a single thread on a single server.

2. Head-of-Line Blocking (The Poison Message)

Imagine the first message in your queue triggers a bug or hits a 3rd party API that times out (30 seconds). In a FIFO system, every other message waits. The entire factory line stops because one widget is stuck. In a "Competing Consumers" model (standard queues), only that one message is delayed; the 100 messages behind it flow to other consumers.

3. The Availability Math (99.99% is Impossible)

This is where the math gets brutal. Strict FIFO requires a Single Active Consumer. If that consumer crashes, the system must detect the failure, release the lease/lock, and elect a new leader. This failover is not instant; it takes time (30-60 seconds). To achieve 99.99% uptime (Four Nines), you are allowed ~4.3 minutes of downtime per month. A handful of consumer restarts or deployments will burn your entire error budget.

  • Competing Consumers allow Active-Active Multi-AZ deployment (instant failover).
  • FIFO Consumers force Active-Passive deployment. You pay for a backup server that sits idle, effectively burning money to provide worse availability.

The Illusion of Order

Here is the hard truth: You rarely have strict order anyway.

  • Network Jitter: Message B (generated at 10:02) might arrive at the queue before Message A (generated at 10:01) due to network routes or producer load balancing. The queue effectively locks in the "wrong" order.
  • The Retry Paradox: If Message A fails transiently (e.g., DB deadlock) and is retried, it usually goes to the back of the line or a delay queue. Message B processes successfully in the meantime. The moment you introduce retries—which are essential in distributed systems—your strict ordering guarantee evaporates.

Better Alternatives: Embracing Disorder

Instead of forcing the infrastructure to be perfect, let's write code that is resilient. Here are three powerful patterns using C#, Rebus, and Marten (for document persistence).

1. Idempotency (Handling Duplicates)

Assume messages will be delivered more than once. If a message arrives out of order (or is replayed), ensure processing it again is safe.

public class PaymentTakenHandler : IHandleMessages<PaymentTaken>
{
    private readonly IDocumentSession _session;

    public PaymentTakenHandler(IDocumentSession session) => _session = session;

    public async Task Handle(PaymentTaken message)
    {
        var order = await _session.LoadAsync<Order>(message.OrderId);

        // Idempotency Check (Domain-Level):
        // If the order is already marked Paid, this is likely a duplicate delivery 
        // We simply ignore it and return "Success" to remove it from the queue.
        if (order.Status == OrderStatus.Paid)
        {
            return; 
        }

        order.MarkPaid(message.Amount, message.TransactionId);
        await _session.SaveChangesAsync();
    }
}

2. Causality Checks (The "Retry Later" Pattern)

If a message arrives "too early" (e.g., OrderShipped before OrderPlaced), don't crash. Just check if the prerequisite exists. If not, defer the message.

public class OrderShippedHandler : IHandleMessages<OrderShipped>
{
    private readonly IBus _bus;
    private readonly IDocumentSession _session;

    public OrderShippedHandler(IBus bus, IDocumentSession session)
    {
        _bus = bus;
        _session = session;
    }

    public async Task Handle(OrderShipped message)
    {
        var order = await _session.LoadAsync<Order>(message.OrderId);

        // Causality Check: We can't ship an order we don't know about yet.
        // It likely exists, but the "OrderPlaced" message is stuck directly behind this one.
        if (order == null)
        {
            // Defer the message for 10 seconds to let the prerequisite arrive.
            // This is "soft" ordering handling.
            await _bus.Defer(TimeSpan.FromSeconds(10), message);
            return;
        }

        order.MarkShipped(message.ShippedAt);
        await _session.SaveChangesAsync();
    }
}

3. Sagas (Order Independence)

For complex workflows, use a Saga (State Machine). A Saga can accept events in any order and only complete when the full state is satisfied.

public class OnboardingSaga : Saga<OnboardingSagaData>,
    IAmInitiatedBy<UserRegistered>,
    IAmInitiatedBy<EmailVerified>
{
    // ... Rebus Saga configuration ...

    public async Task Handle(UserRegistered message)
    {
        Data.UserId = message.UserId;
        Data.ProfileCreated = true;
        await CheckCompletion();
    }

    public async Task Handle(EmailVerified message)
    {
        Data.EmailVerified = true;
        await CheckCompletion();
    }

    private async Task CheckCompletion()
    {
        // Logic runs only when ALL requirements are met, 
        // regardless of whether EmailVerified came before or after UserRegistered
        if (Data.ProfileCreated && Data.EmailVerified)
        {
            await _bus.Publish(new UserOnboardingCompleted { UserId = Data.UserId });
            MarkAsComplete();
        }
    }
}

Conclusion

Strict FIFO queues are a crutch that limits your system's potential. By letting go of the need for strict infrastructure ordering, you gain:

  • Limitless Scalability: Competing consumers can chew through backlogs in parallel.
  • High Availability: Active-Active redundancy across zones.
  • Resilience: The ability to withstand "Poison Messages" without halting the entire system.

The world is chaotic. Don't fight it with brittle queues. Embrace it with resilient patterns.

You May Also Like


Modernizing Legacy Applications with AI: A Specification-First Approach

legacy-vbnet.png
Brad Jolicoeur - 01/03/2026
Read

Transform Your Documentation Workflow with AI: A Hands-On GitHub Copilot Workshop

ai-document-context.png
Brad Jolicoeur - 01/01/2026
Read

Exploring Squirrel: A Fluent Data Analysis Library for .NET

squirrel-explore.png
Brad Jolicoeur - 11/27/2025
Read