SkillPilotAI

Case Study 7: IRCTC Tatkal Ticket Booking under Extreme Spike

At 10 AM lakhs of people rush for limited Tatkal seats at the same moment; the system must stay fair, fast and bot-proof.

C.1 Requirements

Functional requirements:

  • The user logs in, searches trains and sees seat availability.
  • Accept booking requests the moment the Tatkal window opens (AC 10:00, Non-AC 11:00).
  • Virtual waiting room: keep excess users in a queue and let them in when their turn comes.
  • Temporarily lock the seat until payment completes (for example 10-15 min).
  • Take payment through a payment gateway / UPI and generate the PNR on success.
  • On payment failure or timeout, release the seat lock and return the seat to the pool.
  • Stop misuse by bots and agents (CAPTCHA, rate limit, per-user booking limit).
  • Send the PNR confirmation by SMS / email.

Non-functional requirements:

  • Handle 10-15 lakh concurrent users at peak without crashing (graceful degradation via a queue).
  • Zero overselling: one seat to exactly one passenger; strong consistency on inventory.
  • Fairness: first come, first served (FIFO queue); bots must not get an advantage.
  • Seat lock + booking API p99 latency < 500 ms once the user is let in.
  • Exactly-once booking with payment: no double charge or double PNR.
  • 99.95% availability during the Tatkal window.

C.2 Estimation (back-of-the-envelope)

ItemNumberHow it is derived
Peak concurrent users~15 lakhMost Tatkal traffic arrives in the first 2-5 minutes of the window (assumption).
Tatkal seats per day~2-3 lakh berthsAssumption: ~3000 trains x ~80-100 Tatkal berths on average.
Peak incoming requests~5 lakh req/sec15 lakh users x a refresh/poll every 3 sec = 5 lakh RPS at the edge.
Admit rate from waiting room~5,000 users/secThe booking core can safely handle only this much; the rest wait in the queue.
Seat lock writes~3,000/sec peakAbout 60% of admitted users select a seat and lock it.
Booking storage per day~1.5 GB~15 lakh total bookings/day x ~1 KB record.
Redis lock memory~300 MB3 lakh active locks x ~1 KB (key + passenger meta); fits easily in a single cluster.

C.3 API design

EndpointPurpose
GET /v1/trains/search?from&to&dateView trains and Tatkal availability (cached)
POST /v1/queue/joinGet a token in the waiting room; the response has position + ETA
GET /v1/queue/status?tokenPoll the queue position; when your turn comes you get a signed admit-token
POST /v1/bookings/holdLock the seat with the admit-token (Idempotency-Key header)
POST /v1/bookings/{id}/payInitiate payment; returns the gateway redirect URL
POST /v1/payments/callbackGateway webhook: confirm or release the booking on success/fail
GET /v1/pnr/{pnr}View the PNR status

C.4 Data model

Table / StoreFieldsWhy
seat_inventory (Redis + RDBMS, sharded by train_id+date)train_id, journey_date, coach, berth_no, quota, status (FREE/HELD/BOOKED), hold_id, versionEach train-date's inventory sits in a separate shard so one hot train does not slow the whole DB.
seat_holds (Redis with TTL)hold_id, user_id, train_id, date, berths[], expires_atOn a payment timeout the lock expires by itself through the TTL.
bookings (RDBMS, sharded by user_id)booking_id PK, pnr, user_id, train_id, date, passengers JSON, amount, status, idempotency_key UNIQUEACID is needed for payment and PNR consistency.
queue_tokens (Redis sorted set)token_id, user_id, score = arrival_ts, stateA ZSET gives FIFO order and rank in O(log N).
payments (RDBMS)payment_id, booking_id, gateway_txn_id, amount, status, created_atAn audit trail for reconciliation and refunds.

C.5 High-level architecture

IRCTC Tatkal: architecture and request flow
  1. 1.Clients: Mobile App, Web Browser, Bots (scripts, auto-refresh)
  2. 2.Edge: CDN (static pages, queue status cached) and WAF + Bot Manager (IP reputation, fingerprint, behaviour); bots blocked at the edge, suspicious users get CAPTCHA
  3. 3.Global Load Balancer -> Virtual Waiting Room (POST /queue/join, FIFO token, admit N/sec) -> API Gateway + Rate Limiter (admit-token verify, per-user / IP limit)
  4. 4.Services: Search Service (cached availability), Booking Service (hold, confirm, release), PNR Service, Payment Service (idempotent, webhook) -> external Payment Gateway (UPI / card)
  5. 5.Data: Redis (locks via Lua, queue ZSET, seat_holds TTL 15 min, availability cache), Inventory DB (sharded train_id + date), Bookings DB (bookings, payments)
  6. 6.Kafka: booking_confirmed, payment_failed -> Notification Worker (SMS / email), Reconciliation Job (settlement, auto-refund); Hold Expiry Worker (TTL expire -> hold EXPIRED, seat FREE)

Booking path: edge -> waiting room -> admit-token -> hold -> pay -> confirm -> notify. Funnel: ~5 lakh req/s at the edge (15 lakh users) -> ~5,000 admit/s from the waiting room -> ~3,000 locks/s (Redis seat hold); 2-3 lakh Tatkal berths/day.

  • Edge: CDN static pages + cached queue status; the WAF + Bot Manager blocks bots at the edge and gives suspicious users a CAPTCHA.
  • LB -> Virtual Waiting Room: a token in a Redis ZSET; it admits at a fixed rate of ~5,000/s and issues a signed JWT admit-token.
  • The API Gateway verifies the token and passes the request to Booking Service; seat lock via Redis Lua with a 15 min TTL, the hold in the Inventory DB.
  • Payment Service takes payment via the gateway; on the webhook, BOOKED + PNR in the Bookings DB, with notification and reconciliation through Kafka.
  • The Hold Expiry Worker sets the seat back to FREE when the Redis key expires.
ComponentWhat it does
Virtual Waiting RoomWhen the window opens it gives everyone a random/FIFO token and admits users at a fixed rate. Issues a signed JWT admit-token.
WAF + Bot ManagerBlocks automated scripts at the edge using IP reputation, device fingerprint, behaviour analysis and CAPTCHA.
Search ServiceServes the train list and approximate availability from the Redis/CDN cache, so the DB takes no load.
Booking ServiceSeat hold, confirm and release logic. Locks the seat atomically with a Redis Lua script, then persists to the DB.
Payment ServiceIntegrates with the gateway, initiates payment with an idempotency key and verifies the webhook.
Hold Expiry WorkerScans expired holds, sets the seat back to FREE and shows the inventory to waiting users.
KafkaDelivers events like booking-confirmed and payment-failed asynchronously to notification, analytics and reconciliation.

C.6 Main flows

Getting in through the waiting room:

  1. At 9:58 the user logs in; the WAF + CAPTCHA do the bot check.
  2. At 10:00 the user does POST /queue/join and a token with the arrival time is added to the Redis ZSET.
  3. Pre-window arrivals are randomized so those who refreshed early get no unfair advantage.
  4. The client polls the status every 5-10 sec (a lightweight, CDN-cacheable endpoint).
  5. Every second the admission controller marks the top N tokens ADMITTED (N = backend capacity).
  6. The admitted user gets a short-lived signed admit-token (for example valid for 10 min).
  7. The API Gateway verifies the admit-token on every booking call; requests without a token are rejected.

Seat lock and payment flow:

  1. The user selects train, class and passengers and sends POST /bookings/hold with an Idempotency-Key.
  2. Booking Service runs a Redis Lua script: check free berths + mark HELD + SET the hold key with a 15 min TTL, all atomic.
  3. The hold record is saved in the DB in the PENDING_PAYMENT state.
  4. The user goes to the payment page; Payment Service creates an order at the gateway.
  5. The gateway webhook arrives: verify the signature, dedup on payment_id.
  6. On success, in a DB transaction the seats become BOOKED, the PNR is generated and the hold is deleted.
  7. A booking_confirmed event goes on Kafka and the Notification Worker sends SMS/email.
  8. If the TTL expired and the payment arrived late, an auto-refund is triggered.

Payment timeout flow:

  1. The user does not complete payment within 15 min.
  2. The Redis key expires; a keyspace event or the Hold Expiry Worker detects it.
  3. The worker marks the hold EXPIRED in the DB and returns the seats to the inventory as FREE (with a version check).
  4. The availability cache is updated and the next admitted users see the seat.
  5. If a late payment success arrives afterwards, the reconciliation job initiates a refund.

C.7 Deep dives

10:00 thundering herd: Virtual Waiting Room, fixed rate admit
  1. 1.15 lakh users: 9:58 login + CAPTCHA, 10:00 queue/join
  2. 2.Redis ZSET queue_tokens, score = arrival_ts: rank 1 u_881 ADMITTED ... rank 5000 u_771 ADMITTED, rank 5001 u_093 WAITING ...
  3. 3.Admission Controller: top N every second, N = 5,000
  4. 4.Signed admit-token: JWT, user-bound, valid ~10 min
  5. 5.API Gateway: verifies the token on every booking call; no token = reject, so the backend is never overloaded

Pre-window arrivals are randomized: no unfair advantage for early refreshers. Status poll every 5-10 s, CDN-cacheable.

Seat lock + payment happy path
  1. 1.1. POST /bookings/hold: Idempotency-Key + admit-token
  2. 2.2. Redis Lua atomic: FREE? -> HELD + hold key TTL 15 min
  3. 3.3. DB: hold PENDING_PAYMENT; Inventory + version column
  4. 4.4. Payment Service: order created at the gateway
  5. 5.5. Webhook: signature verify, payment_id dedup
  6. 6.6. DB txn: BOOKED + PNR, hold delete -> Kafka booking_confirmed

Two people on the same berth: only one gets HELD in the Lua check-and-set. The DB also has an optimistic lock on status + version, so no overselling. Hot train: a separate Redis shard + coach-level partition.

Payment timeout: seat states
  1. 1.FREE -> Lua hold on inventory -> HELD (hold_id, TTL 15 min)
  2. 2.HELD -> pay success -> BOOKED (PNR)
  3. 3.HELD -> 15 min, no pay -> EXPIRED (hold record, Hold Expiry Worker, version check) -> seat FREE
  4. 4.The next admitted users see the seat: availability cache update

If a payment succeeds late, after the TTL, the reconciliation job matches the settlement file and auto-refunds.

Problem: thundering herd at 10:00, backend crash. The virtual waiting room absorbs the traffic and admits at a fixed rate. Static pages come from the CDN, queue status is cacheable. The backend never gets more load than its capacity.

Problem: overselling / the same berth to two people. The check-and-set in the Redis Lua script is atomic. The DB also has optimistic locking on the status + version column. Inventory is sharded by train_id+date so lock contention stays limited to one shard.

Problem: fairness and bot protection. Bot Manager (fingerprint, behaviour), CAPTCHA, per-user and per-IP rate limits, limited bookings per user per window, and randomizing pre-window arrivals. The admit-token is bound to the user and cannot be shared.

Problem: payment succeeds but the booking fails (or the reverse). Idempotency key + a payment state machine. Both the webhook and the client callback are idempotent. Daily reconciliation matches the gateway settlement file and auto-refunds on a mismatch.

Problem: a hot train (everyone piles onto one popular train). Put that train's inventory on a separate Redis shard. Coach-level partitioning spreads the locks. Read availability from an eventually consistent cache; only the hold gets a strong check.

C.8 Trade-offs

DecisionWhat was chosen and why
Waiting room vs sirf autoscalingChose the waiting room: the spike arrives in seconds, autoscaling is not that fast, and the DB does not scale.
Redis lock vs DB row lockRedis for the fast atomic lock, the DB as the source of truth. If Redis crashes, the DB version check prevents overselling.
Strict FIFO vs randomized entryUsers arriving before the window opens are randomized, after that FIFO; this reduces the advantage of refresh-spam and bots.
Lock TTL lamba vs chhotaAround 15 min: with a shorter TTL genuine users cannot finish paying, with a longer one seats stay blocked for nothing.

C.9 Interview tips

  • Say first that the problem is not read-heaviness but write contention and a spike; that is why the waiting room is the most important piece.
  • Explain the seat hold as a state machine: FREE -> HELD -> BOOKED / EXPIRED.
  • Two layers against overselling: a Redis atomic lock + DB optimistic locking.
  • Be sure to mention the payment timeout and late-payment refund edge case.
  • Present bot protection as a combination of WAF, CAPTCHA, rate limits and a per-user limit.