DEV Community

Cover image for What Really Happens When 25 Lakh People Click “Book” at the Same Time?
Harsh Mangalam
Harsh Mangalam

Posted on

What Really Happens When 25 Lakh People Click “Book” at the Same Time?

Imagine this.

Tatkal booking opens at 10:00 AM.

At exactly 10:00:00, around 25 lakh people click the Book Now button.

There are only 4 seats left in your coach.

So the obvious question is:

How does the system decide which 4 people get the seats?

Is it literally first-come-first-served at the millisecond level?

And what happens to the other 24,99,996 requests?

This is a great system design problem because it teaches several important concepts at once:

  • load balancing
  • rate limiting
  • stateless services
  • race conditions
  • atomic operations
  • queues
  • hot keys
  • partitioning
  • backpressure
  • source of truth

Let’s build the system slowly, one problem at a time.


First, forget the database

A beginner might imagine the system like this:

25 lakh users
      |
      v
   Database
Enter fullscreen mode Exit fullscreen mode

That would be a disaster.

A real system usually has several layers:

User
  |
  v
Internet
  |
  v
Edge / CDN / WAF
  |
  v
Load Balancer
  |
  v
API Servers
  |
  v
Booking Service
  |
  v
Inventory Service
  |
  v
Database
Enter fullscreen mode Exit fullscreen mode

Each layer exists because the simpler design eventually breaks.

Let’s understand why.


1. Who actually clicked first?

Suppose two users click:

User A clicks at 10:00:00.001
User B clicks at 10:00:00.009
Enter fullscreen mode Exit fullscreen mode

It looks like User A should win.

But now include network latency.

User A network latency = 100 ms
User B network latency = 20 ms
Enter fullscreen mode Exit fullscreen mode

Their requests reach the booking system at:

User A → 10:00:00.101
User B → 10:00:00.029
Enter fullscreen mode Exit fullscreen mode

User B reaches the server first.

So:

The system usually cannot guarantee ordering based on the exact physical moment someone clicked.

The user's laptop is outside the system's control.


Why not send the click timestamp?

You might think the browser could send:

{
  "clicked_at": "10:00:00.001"
}
Enter fullscreen mode Exit fullscreen mode

But clients cannot be trusted.

A modified browser could simply send:

{
  "clicked_at": "09:59:59.000"
}
Enter fullscreen mode Exit fullscreen mode

So authoritative decisions should happen on the server.

This is a useful rule far beyond ticket booking:

Never trust the client for critical state.

Examples include:

Price
Permissions
Wallet balance
Inventory
Seat availability
Booking status
Enter fullscreen mode Exit fullscreen mode

2. The first problem is not seats

Before worrying about the 4 seats, there is a more immediate problem:

25 lakh requests just arrived.

If all of them enter your backend, your servers may collapse before anyone gets a ticket.

So the first layer protects the system.

25 lakh users
      |
      v
+----------------------+
| Edge / Cloudflare    |
|----------------------|
| Rate limiting        |
| DDoS protection      |
| Bot detection        |
| Traffic filtering    |
+----------+-----------+
           |
           v
        Backend
Enter fullscreen mode Exit fullscreen mode

This is called admission control.


3. Admission control: don't accept unlimited work

Imagine a nightclub with capacity for 500 people.

There are 20,000 people waiting outside.

You would not allow all 20,000 inside and then decide what to do.

There is a bouncer.

In distributed systems, the "bouncer" could be:

Rate limiting
Bot protection
Concurrency limits
Queues
Load shedding
Enter fullscreen mode Exit fullscreen mode

The principle is:

Protect expensive downstream systems by limiting how much work enters.

This pattern appears everywhere:

  • flash sales
  • online exams
  • ticket launches
  • gaming events
  • IPO applications
  • payment systems

4. Then comes the load balancer

One server cannot handle millions of requests.

So we run many API servers.

                Load Balancer
                     |
        ---------------------------
        |            |            |
        v            v            v
      API-1        API-2        API-100
Enter fullscreen mode Exit fullscreen mode

The load balancer spreads requests across them.

This is horizontal scaling.

Instead of buying one giant server, we add more servers.

If one server handles roughly:

5,000 requests/second
Enter fullscreen mode Exit fullscreen mode

then 100 servers could theoretically handle around:

500,000 requests/second
Enter fullscreen mode Exit fullscreen mode

Actual capacity depends on the workload, but the pattern is what matters.


5. API servers should be stateless

Here is an important mistake.

Suppose API Server 1 stores this in memory:

availableSeats = 4
Enter fullscreen mode Exit fullscreen mode

And API Server 2 also stores:

availableSeats = 4
Enter fullscreen mode Exit fullscreen mode

Now both servers might independently sell those four seats.

That is obviously wrong.

So critical shared state should not live independently inside each API server.

Instead:

API-1 -----\
API-2 ------\
API-3 -------> Shared Inventory Service
API-4 ------/
Enter fullscreen mode Exit fullscreen mode

The API servers are mostly stateless.

This makes scaling easier because any request can go to any server.

If Server 27 crashes, the load balancer simply routes traffic elsewhere.

A useful mental model is:

Stateless compute is easy to scale. Shared mutable state is where distributed systems become difficult.


6. Now we reach the real problem: race conditions

Suppose only one seat remains.

availableSeats = 1
Enter fullscreen mode Exit fullscreen mode

Two servers receive booking requests almost simultaneously.

Server A does:

READ availableSeats
Enter fullscreen mode Exit fullscreen mode

It sees:

1
Enter fullscreen mode Exit fullscreen mode

Server B also does:

READ availableSeats
Enter fullscreen mode Exit fullscreen mode

It also sees:

1
Enter fullscreen mode Exit fullscreen mode

Both then say:

seat is available
book it
Enter fullscreen mode Exit fullscreen mode

Now one seat has been sold to two users.

This is a race condition.

The timeline looks like:

Time →

Server A    READ seats = 1
Server B        READ seats = 1
Server A            WRITE seats = 0
Server B                WRITE seats = 0
Enter fullscreen mode Exit fullscreen mode

Both requests believed they succeeded.


7. The fix: atomic operations

The mistake above is that we did this:

READ
CHECK
WRITE
Enter fullscreen mode Exit fullscreen mode

as separate steps.

Instead, the check and update should behave like one indivisible operation.

Conceptually:

IF seats > 0
THEN seats = seats - 1
Enter fullscreen mode Exit fullscreen mode

One database implementation might look like:

UPDATE inventory
SET available_seats = available_seats - 1
WHERE train_id = ?
  AND available_seats > 0;
Enter fullscreen mode Exit fullscreen mode

Suppose one seat remains.

Request A executes first:

1 → 0
Enter fullscreen mode Exit fullscreen mode

Success.

Request B executes next.

The condition:

available_seats > 0
Enter fullscreen mode Exit fullscreen mode

is false.

So Request B fails.

No overselling.

This property is called atomicity.

Either the whole operation happens or none of it happens.


8. Great, so let 25 lakh requests hit this SQL query?

Not so fast.

The query may be logically correct, but there is another problem.

All 25 lakh users want the same resource.

25 lakh requests
        |
        v
+----------------------+
| Train 12952          |
| Tatkal 3A            |
| available_seats = 4  |
+----------------------+
Enter fullscreen mode Exit fullscreen mode

This single inventory record becomes extremely hot.

This is called a:

  • hot row
  • hot key
  • hotspot

Even if you have 10,000 application servers, they may all eventually fight over the same inventory record.

10,000 API servers
        |
        v
 same inventory row
Enter fullscreen mode Exit fullscreen mode

This teaches an important lesson:

Scaling your application servers does not automatically scale shared state.

You see the same problem in:

  • concert tickets
  • Amazon flash sales
  • limited sneaker drops
  • coupon redemption
  • wallet balances
  • stock trading

9. Use a queue to absorb the burst

Instead of directly hammering the inventory system:

25 lakh requests
       |
       v
Inventory
Enter fullscreen mode Exit fullscreen mode

we can place a queue in front:

25 lakh requests
       |
       v
+----------------+
| Booking Queue  |
+-------+--------+
        |
        v
     Workers
        |
        v
    Inventory
Enter fullscreen mode Exit fullscreen mode

Now the huge incoming burst can be absorbed temporarily.

Requests may enter the queue as:

R1
R2
R3
R4
R5
...
Enter fullscreen mode Exit fullscreen mode

Suppose four seats exist.

R1 → seat → 3 left
R2 → seat → 2 left
R3 → seat → 1 left
R4 → seat → 0 left
R5 → sold out
Enter fullscreen mode Exit fullscreen mode

The important transformation is:

Huge uncontrolled concurrency
Enter fullscreen mode Exit fullscreen mode

becomes:

Controlled processing
Enter fullscreen mode Exit fullscreen mode

10. Why queues are useful

Queues decouple two different rates:

Rate at which requests arrive
Enter fullscreen mode Exit fullscreen mode

and:

Rate at which the backend can process requests
Enter fullscreen mode Exit fullscreen mode

Without a queue:

Traffic spike
>>>>>>>>>>>>>>>>>>>>>>>> DATABASE
Enter fullscreen mode Exit fullscreen mode

With a queue:

Traffic spike
>>>>>>>>>>>>>>>>>>>>>

        |
        v

+----------------------+
|        Queue         |
| |||||||||||||||||||  |
+----------+-----------+
           |
           v

       Workers

----> ----> ----> Database
Enter fullscreen mode Exit fullscreen mode

This is also a form of backpressure.

The downstream system says, in effect:

I will process work at the speed I can safely handle.


11. Should there be one global queue?

Probably not.

Imagine one queue for every booking in India.

Then a Delhi–Mumbai booking could block a completely unrelated Chennai–Bengaluru booking.

That wastes parallelism.

Instead, we partition the work.

A possible inventory key could be:

train_id + journey_date + class + quota
Enter fullscreen mode Exit fullscreen mode

For example:

12952:2026-08-20:3A:TATKAL
Enter fullscreen mode Exit fullscreen mode

All requests for the same inventory key should follow the same ordering path.

Different trains can be processed independently.

Train A → Partition 1 → Worker 1
Train B → Partition 2 → Worker 2
Train C → Partition 3 → Worker 3
Enter fullscreen mode Exit fullscreen mode

This is partitioning or sharding.


12. The magic idea: serialize only conflicting work

This is one of the most reusable ideas in system design.

We do not need to serialize every booking in the country.

We only need to serialize requests that compete for the same inventory.

So instead of:

Every booking
    |
    v
one global worker
Enter fullscreen mode Exit fullscreen mode

we do something closer to:

Same train/date/class/quota
          |
          v
   ordered processing
Enter fullscreen mode Exit fullscreen mode

while unrelated inventory runs in parallel.

A good interview phrase is:

Serialize operations per inventory key, not globally.

That gives us both:

Correctness
+
Parallelism
Enter fullscreen mode Exit fullscreen mode

13. So which four people actually win?

Now we can finally answer the original question.

The system may create an authoritative order somewhere inside its infrastructure.

For example:

User B → sequence 91821
User X → sequence 91822
User A → sequence 91823
User Z → sequence 91824
User P → sequence 91825
Enter fullscreen mode Exit fullscreen mode

There are four seats.

So:

91821 → gets seat
91822 → gets seat
91823 → gets seat
91824 → gets seat
91825 → sold out
Enter fullscreen mode Exit fullscreen mode

The exact real-world click ordering may be impossible to know perfectly.

What matters is that the system defines one reliable ordering at a controlled point.

This leads to a broader distributed systems idea:

Sometimes we create an authoritative ordering instead of trying to discover the absolute real-world ordering.


14. What happens to the other 24,99,996 requests?

We ideally do not let every losing request perform an expensive database transaction.

Once the system confidently knows:

inventory = 0
Enter fullscreen mode Exit fullscreen mode

future work can often be rejected earlier.

Instead of:

User
 |
 v
API
 |
 v
Queue
 |
 v
Worker
 |
 v
Database
 |
 v
Sold out
Enter fullscreen mode Exit fullscreen mode

we may eventually do:

User
 |
 v
Booking Service
 |
 v
Sold out
Enter fullscreen mode Exit fullscreen mode

This is called fail fast.

If an operation clearly cannot succeed, reject it as early and cheaply as possible.

This saves:

  • CPU
  • database connections
  • queue capacity
  • network calls
  • locks
  • memory

15. But be careful with caching

Suppose we cache:

Tatkal inventory = SOLD OUT
Enter fullscreen mode Exit fullscreen mode

That is useful for fast rejection.

But what happens if someone fails payment?

The seat might become available again.

So we need to distinguish:

Cache
Enter fullscreen mode Exit fullscreen mode

from:

Source of truth
Enter fullscreen mode Exit fullscreen mode

A cache is fast, but it may be stale.

The authoritative inventory system decides the truth.

A good rule is:

Use caches to make the system faster, not to accidentally create a second source of truth.


16. Booking is not the same as confirmation

There is another important complication.

Imagine four users get seats, but payment takes two minutes.

Should those seats be permanently gone immediately?

Usually you need a temporary reservation.

The booking may move through states like:

AVAILABLE
    |
    v
HELD
    |
    v
PAYMENT_PENDING
    |
    +-----------+
    |           |
    v           v
CONFIRMED     FAILED
                |
                v
            AVAILABLE
Enter fullscreen mode Exit fullscreen mode

Example:

4 seats

A → held
B → held
C → held
D → held

available = 0
Enter fullscreen mode Exit fullscreen mode

Later:

A payment succeeds → confirmed

B payment fails → seat released

C succeeds → confirmed

D times out → seat released
Enter fullscreen mode Exit fullscreen mode

This introduces several new system design topics:

  • TTLs
  • reservation expiry
  • state machines
  • retries
  • payment failures
  • compensation

These are natural follow-ups once the basic booking flow is correct.


17. Final high-level architecture

Putting everything together:

                       USERS
                         |
                         v
               +-------------------+
               | Edge / Cloudflare |
               |-------------------|
               | DDoS protection   |
               | Rate limiting     |
               | Bot detection     |
               +---------+---------+
                         |
                         v
               +-------------------+
               |   Load Balancer   |
               +---------+---------+
                         |
          +--------------+--------------+
          |              |              |
          v              v              v
       API-1           API-2          API-N
          \              |              /
           \             |             /
            +------------+------------+
                         |
                         v
               +-------------------+
               | Booking Service   |
               +---------+---------+
                         |
                         v
               +-------------------+
               | Booking Queue     |
               | partitioned by    |
               | inventory key     |
               +---------+---------+
                         |
             +-----------+-----------+
             |           |           |
             v           v           v
          Worker 1    Worker 2    Worker N
             |           |           |
             +-----------+-----------+
                         |
                         v
               +-------------------+
               | Inventory Service |
               +---------+---------+
                         |
                         v
               +-------------------+
               |    Database       |
               | Source of Truth   |
               +-------------------+
Enter fullscreen mode Exit fullscreen mode

18. What did we actually learn?

The interesting part is not memorizing the architecture.

The important part is understanding why each component appeared.

Problem Pattern
Millions of incoming requests Horizontal scaling
Backend may collapse Admission control
Bots generate unfair traffic Rate limiting / bot protection
Many API servers Stateless services
Two users can get the same seat Atomicity
Millions hit the same inventory Hotspot recognition
Huge burst at 10 AM Queue
Backend slower than incoming traffic Backpressure
Independent trains should run separately Partitioning
Same inventory needs ordering Per-key serialization
Inventory already exhausted Fail fast
Repeated reads are expensive Caching
System needs one correct answer Source of truth

19. The most important system design habit

Do not start with:

We need Kafka.
We need Redis.
We need Cassandra.
Enter fullscreen mode Exit fullscreen mode

Start with:

What problem do I have?
        |
        v
Why does it happen?
        |
        v
What is the simplest solution?
        |
        v
What breaks at scale?
        |
        v
What guarantee do I need?
        |
        v
Which system design pattern gives me that guarantee?
        |
        v
What new tradeoff did I introduce?
Enter fullscreen mode Exit fullscreen mode

For example:

25 lakh requests
    ↓
backend overload
    ↓
admission control
Enter fullscreen mode Exit fullscreen mode

Then:

traffic arrives faster than backend processes
    ↓
queue
Enter fullscreen mode Exit fullscreen mode

Then:

multiple servers update same seat
    ↓
race condition
    ↓
atomic operation
Enter fullscreen mode Exit fullscreen mode

Then:

everyone hits same inventory
    ↓
hotspot
    ↓
partition + serialize per key
Enter fullscreen mode Exit fullscreen mode

That way, the architecture becomes a consequence of the problem instead of something you memorize.


20. The one sentence to remember

The hard part is not receiving 25 lakh requests. The hard part is safely coordinating millions of concurrent requests around a tiny amount of shared mutable state.

Once this idea makes sense, a lot of other systems start looking familiar:

  • concert ticketing
  • hotel bookings
  • airline seats
  • flash sales
  • stock trades
  • wallet balances
  • coupon redemption

The domain changes.

The underlying system design patterns repeat.

Top comments (0)