DEV Community

Cover image for Building a Flash Sale System That Survived 7,000 Requests/sec
Aniruddha Gawali
Aniruddha Gawali

Posted on AI-assisted

Building a Flash Sale System That Survived 7,000 Requests/sec

This is a series where I am starting to share the best practices for backend development, its architecture, and system design for high throughput across the system with minimum cost.

I have divided this into 5 mini projects where we can learn with the help of the Iterative Refinement process, which will help us to know which issues can be come in real life production applications.

First of all, we will start with the project which is database-driven and covers concepts like Concurrency, Race Conditions, Locks.


📚 Table of Contents


Basic Setup for Learning:

Setting up the DB config & minimal API

  • Spin up a DB on your local computer with the help of Docker; I have used Postgres for this with Docker Compose.

  • Create the tables in the DB for movies and orders; movies must have the tickets available count, and the orders table is for saving ticket details. Each ticket has a maximum of 100 seats.

docker-compose.yml

services:
  db:
    image: postgres:alpine
    container_name: alpine_postgres
    restart: always
    environment:
      POSTGRES_USER : postgres
      POSTGRES_PASSWORD: strongPass@123
      POSTGRES_DB : my_db
    ports:
      - "5432:5432"
    volumes:
      - postgress_data:/var/lib/postgresql

volumes:
  postgress_data:

Enter fullscreen mode Exit fullscreen mode

Create a simple API

  • Create a simple API that will take input from the user for a ticket, which has a name, quantity of tickets, and movie ID for which it has to buy the tickets.

  • Add the logic: if available_tickets - qty > 0, then
    only the order should be created, and the quantity of seats should be reduced; otherwise, give the error: "tickets not available".

Ticket booking API logical flow

Load testing

  • Use the K6 library for load testing by sending 1,000 simultaneous requests to your API endpoint at the exact same moment.

  • Expected Result: You will experience a massive race condition. Because hundreds of requests read the database at the exact same millisecond, they all saw 100 tickets available. They all passed the check, and they all subtracted 1. When the dust settles, your database will show negative available tickets, and you will have hundreds of successful orders in your Orders table.

load-test.js


import http from 'k6/http';
import { check } from 'k6';

// 1. Configure the Load
export const options = {
  // We want to simulate 1,000 users hitting the button at the exact same time
  vus: 1000, 
  // We will sustain this pressure for 5 seconds
  duration: '5s', 
};

// 2. Define the User Action
export default function () {
  const url = 'http://localhost:8080/book';

  const payload = JSON.stringify({
    movie_id:1,
    name:"Ani",
    qty:1
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  // Execute the POST request
  const res = http.post(url, payload, params);

  // Verify the server responded with a 200 OK
 check(res, {
  'is status 2xx': (r) => r.status >= 200 && r.status <= 299,
});
}

Enter fullscreen mode Exit fullscreen mode

What actually happens!!!:

Ran an initial stress test with k6 blasting 1,000 Virtual Users (VUs) for 5 seconds.

checks_failed: 99.98% (159575 out of 159606)
Postgres working: conn busy
http: panic serving: watch already in progress
Enter fullscreen mode Exit fullscreen mode

Why did this happen!?

Because a single, raw database connection was shared globally across the HTTP handler. Thousands of concurrent and simultaneous reads and writes were attempted on the exact same underlying TCP socket. The database driver flagged this concurrent socket access as conn busy.

What the Fix

  • Migrated from a single connection to a connection pool

Transactions and Concurrency

Now re-ran the stress test with the connection pool config and got the new expected error; the HTTP layer was stable: 71,996 requests processed at ~14,266 req/s with 0% HTTP errors, but the ticket count ended at -770, and hundreds of invalid orders were created.

Why did this happen? Because hundreds of concurrent queries executed the SELECT query at the exact same microsecond before any single transaction had committed its update. Every query read count = 100, evaluated 100 > 0 as true in memory, and executed an UPDATE, completely stomping over concurrent writes. Demonstrates why application-layer conditional checks (if tickets > 0) cannot guarantee transactional isolation under concurrency.

To prevent data race conditions, add explicit transactions combined with atomic updates.

Ticket booking API logical flow with transaction logic


After this, update the current logic with the transactional combination and re-run the K6 stress test. It handled every request perfectly; throughput was ~7,176 requests/second, exactly 100 tickets were sold, and the remaining ticket count was locked at exactly 0. There were no deadlocks, no socket panics, and no data corruption.

This is how you build a concurrent ticket booking system or a high-update database like the BookMyShow application, the IRCTC train booking system, or a flight booking systemwith the help of Database Pooling, Transactions and Locks.


What We Learned

  • Never share a single database connection across concurrent HTTP requests.
  • Connection pooling solves socket-level concurrency issues.
  • Application-layer if (tickets > 0) checks are unsafe under heavy concurrency.
  • Atomic SQL updates inside a database transaction prevent overselling.
  • PostgreSQL transactions and row locks guarantee consistency under high write throughput.

Where This Pattern Is Used in Production

This exact pattern is used in systems where inventory or seats are limited:

  • BookMyShow movie ticket booking.
  • IRCTC train reservations.
  • Flight booking systems.
  • Flash sale and limited-stock e-commerce platforms.

What's Next in This Backend Best Practices Series

In the next mini project, we'll move beyond database concurrency and build a high-throughput backend using Redis for distributed locking, idempotency, and request coordination under massive traffic spikes.

Top comments (0)