The 1-Million Request Sandbox: Scale FastAPI & PostgreSQL on Your Laptop

Learn API scaling by building a local sandbox. Follow this step-by-step project guide using FastAPI, PostgreSQL, Redis, and Locust to simulate high-throughput traffic and fix bottlenecks.

Rohit Bairwa

Rohit Bairwa

Author

4 min read
The 1-Million Request Sandbox: Scale FastAPI & PostgreSQL on Your Laptop


Step 1: The Infrastructure Sandbox

To keep your laptop clean, run your databases inside Docker containers. You need PostgreSQL as your primary data store and Redis as your caching layer.


Create a docker-compose.yml file:


YAML

YAML
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: test_user
      POSTGRES_PASSWORD: test_password
      POSTGRES_DB: api_db
    ports:
      - "5432:5432"
  
  redis:
    image: redis:7
    ports:
      - "6379:6379"

Run docker-compose up -d to spin them up.


Step 2: The "Bad" Baseline API

To learn how to fix a bottleneck, you first have to create one. Write a basic FastAPI app with a synchronous database driver (like psycopg2).


Intentionally make the worst possible architectural choice: open a new database connection for every single request.


Python

Python
# main.py
from fastapi import FastAPI
import psycopg2

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    # BAD: Opening a new connection synchronously per request
    conn = psycopg2.connect("dbname=api_db user=test_user password=test_password host=localhost")
    cur = conn.cursor()
    cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    user = cur.fetchone()
    conn.close()
    return {"user": user}

Step 3: Break the API (Load Testing)

Now, simulate massive traffic using an open-source tool like Locust. Locust lets you write Python scripts to simulate thousands of concurrent users hitting your local server.


Create a locustfile.py:


Python

Ruby
from locust import HttpUser, task, between

class APIUser(HttpUser):
    wait_time = between(1, 2)

    @task
    def query_user(self):
        # Pick a random user ID to query
        self.client.get("/users/1")

Run your FastAPI app (uvicorn main:app --workers 1), and then run Locust (locust -f locustfile.py). Navigate to Locust's web UI at http://localhost:8089 and unleash 500 concurrent users.


What you will see: Your laptop's CPU will spike. PostgreSQL will quickly run out of available connections (the default max is usually 100), and FastAPI will start throwing 500 Internal Server Errors. Your Requests Per Second (RPS) will flatline.


Step 4: The Fix — Async I/O & Connection Pooling

Now, refactor the application to handle high concurrency. Swap the synchronous driver for an async driver (asyncpg) and initialize a connection pool when the FastAPI app starts.


A connection pool keeps a set number of database connections open and reuses them across thousands of incoming requests, completely eliminating the connection overhead.


Python

Python
import asyncpg
from fastapi import FastAPI

app = FastAPI()
db_pool = None

@app.on_event("startup")
async def startup():
    global db_pool
    # Create a reusable pool of connections
    db_pool = await asyncpg.create_pool(dsn="postgresql://test_user:test_password@localhost/api_db")

@app.get("/users/{user_id}")
async def get_user_optimized(user_id: int):
    # GOOD: Reuse a connection from the pool asynchronously
    async with db_pool.acquire() as conn:
        user = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
        return {"user": dict(user)}

Rerun the Load Test: Hit it with 500 users again. You will see 0 errors, drastically lower latency, and a much higher RPS. You have successfully decoupled database I/O from your API's main thread.


Step 5: The Final Polish — Redis Caching

To simulate handling millions of read-heavy requests, integrate Redis. Fetching from RAM is exponentially faster than fetching from a disk-backed database.


  1. Add the redis-py library to your project.

  2. In your endpoint, first check if user_id exists in Redis.

  3. If yes (Cache Hit), return it immediately.

  4. If no (Cache Miss), fetch it using your async pool, store the result in Redis with an expiration time (Time To Live / TTL), and return it.

When you run your load test this time, the first request will hit Postgres, and the subsequent 499 requests will be served instantly from Redis. Your latency will drop to mere milliseconds, mimicking a true production-grade API.


Testing FastAPI with 100 Virtual Users Using Locust


This video provides a practical, beginner-friendly walkthrough of setting up Locust to simulate hundreds of concurrent virtual users against a FastAPI application, mirroring the exact load-testing steps outlined in the project.

Rohit Bairwa
Written by

Rohit Bairwa

Published on · 4 min read read

Want this implemented for your product?

Tell us what you're building. We'll help with the stack, timeline, and next steps.