Skip to main content
HomeBlogServicesLinks
RSS

Flash-Booking Systems: Why More Servers Won't Help (Part - 1)

27 Jul 2026
  • Architecture
  • System Design
  • Backend
  • Engineering
  • Cloud

The Setup

Picture this: a national railway system runs a scheme where a small batch of tickets for last-minute, unplanned travel opens up for booking at an exact second, every single day. Not a rolling window. Not "book anytime in the next hour." One precise timestamp - say, 10:00:00 AM - and the moment it hits, every seat on every eligible train for that day becomes bookable.

Now scale the demand. Roughly 2 million people trying to book. Roughly 500 seats available on a popular route. Everyone hits "book" in the same second.

If this sounds like a niche problem specific to one country's railway system, it isn't. It's the exact same shape as:

  • A concert ticketing platform releasing 500 tickets for a sold-out show
  • A sneaker drop where 10,000 people fight for 200 pairs
  • A university exam registration portal opening at midnight for limited seats
  • A limited-edition product restock on an e-commerce site

Same problem, different costume: a fixed, tiny resource, and a demand spike synchronized to the millisecond. If you've built (or debugged) any of the above, you already know this pain in your bones.

This is Part 1 of a three-part deep dive. In this part, we set up the problem correctly and torch the naive approaches before they even get a chance to fail in production. Part 2 walks through the full architecture that actually survives this. Part 3 gets into the edge cases that separate "works in the demo" from "works on the worst day of the year."

Why This Is Not a Throughput Problem

Here's the instinct almost every engineer has the first time they hear this problem: just add more servers. More app instances, bigger database, more connections, a beefier load balancer. Scale horizontally, scale vertically, throw money at it.

This instinct is wrong, and it's worth being blunt about why.

Throughput scaling helps when the bottleneck is compute or bandwidth - things you can add more of. But the actual constraint here is the resource itself: there are 500 seats. Full stop. Adding a thousand more app servers doesn't create seat number 501. You could serve 2 million requests per second with infinite compute and you'd still have exactly 500 seats to hand out and 2 million people wanting one.

So the real engineering problem is not "how do we handle more load," it's "how do we let an enormous, synchronized crowd fairly and correctly compete for a resource that cannot grow." That reframing changes everything about the design. You're not building a system optimized purely for throughput. You're building a system optimized for correctness under contention, where fairness and zero double-booking matter more than raw requests-per-second.

Keep that distinction in your head, because it's the thing that makes every design decision downstream make sense.

What Breaks First: The Naive Approach

Let's walk through the design almost every engineer reaches for first, and watch it fail in slow motion.

Naive attempt #1: Direct database check-then-update

async function bookSeat(userId: string, trainId: string): Promise<BookingResult> {
  const availableSeats = await db.query(
    "SELECT count FROM seat_inventory WHERE train_id = $1 FOR UPDATE",
    [trainId]
  );

  if (availableSeats.count > 0) {
    await db.query(
      "UPDATE seat_inventory SET count = count - 1 WHERE train_id = $1",
      [trainId]
    );
    await db.query(
      "INSERT INTO bookings (user_id, train_id) VALUES ($1, $2)",
      [userId, trainId]
    );
    return { success: true };
  }
  return { success: false, reason: "SOLD_OUT" };
}

This looks correct. FOR UPDATE takes a row lock, so no two transactions can read-and-decrement the same row simultaneously without waiting their turn. In isolation, on a whiteboard, this is textbook-correct.

Here's what actually happens at 2 million concurrent attempts against a handful of hot rows:

  1. Every single request needs a database connection. Your connection pool - even a generous one - caps out somewhere in the hundreds or low thousands. The rest queue.
  2. Every request that reaches the row has to wait for the lock held by the request in front of it. With 2 million requests funneling through effectively one lock per train, your queue depth explodes. What should take milliseconds starts taking minutes.
  3. Your database's transaction log, connection overhead, and context-switching costs balloon under this contention. You're not just slow, you risk the whole database instance choking, which can take down every other train's booking, not just the popular one.
  4. Meanwhile, the 2 million people who didn't even reach the database yet are refreshing, retrying, and doubling your effective load, because a spinner with no feedback makes people click harder, not calmer.

Naive attempt #2: "Let's just let everyone hit the API and see what happens"

No admission control, no waiting room, just raw traffic straight to your app tier. This fails even before it reaches the database. Your load balancer and app servers get flooded with connection attempts in the same second, most of which will fail or time out anyway since only 500 can succeed. You've built a system that spends 99.98% of its capacity serving people a "sorry, sold out" message as fast as possible, which is expensive busywork, not a feature.

Naive attempt #3: Optimistic locking with retries

Some engineers try to dodge lock contention with optimistic concurrency (check a version number, retry on conflict). This sounds better on paper, but at this scale it just moves the problem: instead of queueing on a lock, you get an enormous storm of failed writes and retries hammering the same row, which is arguably worse because now you're doing wasted write attempts instead of orderly waiting.

The common thread in all three failures: none of them address the actual shape of the problem. They're all trying to let 2 million requests directly contend for one shared piece of state, synchronously, in the hot path. That's the fundamental design flaw. The fix isn't a better lock. It's not letting 2 million requests fight over shared state directly in the first place.

Image1

That's where Part 2 picks up: structuring the traffic before it ever reaches your critical resource, so that by the time anything touches "the 500 seats," you're dealing with a controlled, ordered stream instead of an uncoordinated stampede.

Next up in Part 2: the actual architecture - waiting rooms, Redis-based atomic reservations, and how Kafka keeps the source of truth consistent without ever blocking the hot path.

Comments

Add a new comment
Supports markdown
PreviousSystem Design ExplainedScalability Deep Dive: Capacity Planning & Back-of-Envelope MathSystem Design ExplainedNextFlash-Booking Systems: The Architecture That Works (Part 2)

Enjoyed this one?

Subscribe to get posts like this straight to your inbox - no noise, just quality content.

We care about your data. Read our privacy policy.

Stay Connected

GitHub •LinkedIn •X •Daily.dev •Email

© 2026 Chiristo. Feel free to share or reference this post with proper credit