Logo
All posts

How OpenAI scaled Postgres to manage 800 million users

In this blog we will see how one Postgres primary keeps up with ChatGPT, and what OpenAI built around it.

Gaurav Sen/11 min read

In January 2026, OpenAI wrote that ChatGPT's main database was PostgreSQL.[1] It served 800 million users with one primary instance, and its load had grown more than 10x in a year.

Engineers at that scale usually split the database. OpenAI kept one writer, and faced two problems:

  1. Every write went to that one primary.
  2. Every write had to be copied to about 50 replicas.

In this blog we will see how one Postgres primary keeps up with ChatGPT, and what OpenAI built around it.

1. One primary, many replicas

What does the database look like?

OpenAI uses a very simple Postgres architecture with a single primary instance and many read replicas spread across the globe.

This architecture easily scales out horizontally, but the architecture has two major drawbacks.

The primary and its read replicas in one picture, played step by step. Read 51 (green) is added, four writes (the dots) reach the primary together, one write is copied to every replica, and four writes arrive as one batch. Last, a ready-to-take-over instance stands behind the primary.
The primary and its read replicas in one picture, played step by step. Read 51 (green) is added, four writes (the dots) reach the primary together, one write is copied to every replica, and four writes arrive as one batch. Last, a ready-to-take-over instance stands behind the primary.

A replica holds a copy of the data and answers reads. More reads? Add a replica.

But a replica takes no writes. Every write still lands on the primary.

2. A write storm

What happens when many writes arrive together?

One is that if there is a write storm, where many write operations are sent from the application server, the single write instance becomes the bottleneck.

OpenAI mitigated this by scaling up the write instance and moving the write-heavy workloads to a new system using Cosmos DB.

OpenAI's post names the trigger: "a write storm from a new feature launch". It moved the write-heavy workloads that could be split by key to Azure Cosmos DB.

It also stopped adding tables to Postgres. New workloads go to the sharded systems by default.

3. One write becomes 50

The primary now takes fewer writes. Does each one cost it less?

The second drawback is that this system has write amplification, where a single write request has to be copied onto all the replicas, effectively converting one request into 50.

Here, the applications were asked to do "lazy writes", where multiple write operations are collected in batch and then sent to the database as a single write operation.

The copy travels as the primary's write-ahead log (WAL). The primary streams it to every replica, and each stream costs it network and CPU.

So fewer, bigger writes mean less to stream. OpenAI's post says lazy writes "smooth traffic spikes".

4. What if the primary fails?

Have you seen an app stop working when its database did? One writer is one point of failure. If it goes down, every write fails.

So OpenAI runs the primary with a hot standby. It's a replica kept in sync and "always ready to take over serving traffic".

Reads don't wait for the primary at all. Most critical requests only read. They keep working from the replicas while the standby is promoted.

5. A pool of connections

How many connections can the primary hold? Fewer than its clients would open.

OpenAI's post describes incidents of "connection storms that exhausted all available connections".

OpenAI also used connection pooling, which limits the number of connections that can be created with the database by presetting a pool of connections and managing any incoming request with the pool.

This allows multiple clients to use the same connection, while reducing the time required to set up a new connection for a new request.

Clients reach the database only through the four connections of the pool. Conn 4 (orange) is in use, and the others (green) wait for the next request.
Clients reach the database only through the four connections of the pool. Conn 4 (orange) is in use, and the others (green) wait for the next request.

The pooler is PgBouncer, a proxy in front of the database.

6. Many clients, one key

The pool caps the connections. What if a thousand of them ask for the same row?

That happens when a cache fails. Every request misses, and every miss goes to the database at once.

They also used request-collapsing in their caches. Here, when a client asks for a key, the key is stored in cache and the value is a promise / future.

When another client asks for the same data, the cache entry is used to return the future. When the data is retrieved from the database, all of the waiting clients receive the same future, resulting in immediate response.

This avoids making multiple requests to the database for the same key.

Three clients ask the cache for user:42. The cache holds one entry for the key, and its value is a promise. The database answers once.
Three clients ask the cache for user:42. The cache holds one entry for the key, and its value is a promise. The database answers once.

Here are three clients asking for user:42, first with a plain cache and then with a future in it.

import asyncio

calls = 0


async def database(key):
    global calls
    calls += 1
    await asyncio.sleep(0.1)  # the query takes time
    return f"row for {key}"


values = {}


async def plain_get(key):
    if key not in values:
        values[key] = await database(key)  # every client that misses goes to the database
    return values[key]


futures = {}


async def collapsed_get(key):
    if key not in futures:
        futures[key] = asyncio.ensure_future(database(key))  # the value is a future
    return await futures[key]


async def three_clients(get):
    global calls
    calls = 0
    rows = await asyncio.gather(get("user:42"), get("user:42"), get("user:42"))
    return rows, calls


rows, plain = asyncio.run(three_clients(plain_get))
rows, collapsed = asyncio.run(three_clients(collapsed_get))
print("3 clients got", rows[0])
print("plain cache:", plain, "database calls")
print("collapsed:  ", collapsed, "database call")
3 clients got row for user:42
plain cache: 3 database calls
collapsed:   1 database call

All three clients got the same row. With the future in the cache, the database was asked once instead of three times.

OpenAI's post calls its version cache locking and leasing. One request takes the lock and fetches the row, and "all other requests wait for the cache to be updated".

7. Replicas that feed replicas

Can the replicas keep growing? The cache saves the database repeated reads, but the primary still streams its WAL to every replica.

How far can that go? OpenAI's post says it "can't keep adding replicas indefinitely without eventually overloading the primary".

To scale read replicas effectively, OpenAI moved away from simple WAL to a hierarchical WAL structure, where the primary sends write events to some read replicas, which further propagate the events to the remaining read replicas.

This increases the operational complexity of the system but results in faster transmission of all WRITE operations.

The primary sends write events to two read replicas (orange). Each of them passes the events on to two more.
The primary sends write events to two read replicas (orange). Each of them passes the events on to two more.

This is called cascading replication. In January 2026 it was still in the works: OpenAI wrote that it was "collaborating with the Azure PostgreSQL team" on it.

So, how does one primary keep up?

What did OpenAI change, then? The primary got bigger, and it gets less to do. Its write-heavy workloads left for Cosmos DB, and the writes that stayed arrive in batches.

A hot standby waits behind it, a pool caps its connections, and a cache with one fetch per key guards its reads.

Overall these operations have been well-studied in distributed systems. An example of this is Memcached, which powers the entire social graph of Meta.

Summary

  1. OpenAI serves 800 million ChatGPT users from one PostgreSQL primary and nearly 50 read replicas.
  2. In a write storm the single primary is the bottleneck. OpenAI scaled it up and moved write-heavy workloads to Cosmos DB.
  3. Write amplification copies one write to every replica, through the WAL.
  4. Lazy writes collect several writes and send them as one.
  5. A hot standby takes over if the primary fails, and reads keep working from the replicas.
  6. PgBouncer pools the connections. Many clients share a few.
  7. Request collapsing stores a future in the cache. The database is asked once per key.
  8. Cascading replication has some replicas pass the WAL on to the rest. OpenAI was building it in January 2026.

Sources

  1. Scaling PostgreSQL to power 800 million ChatGPT users, Bohan Zhang, OpenAI, 22 January 2026.
  2. Scaling Memcache at Facebook, Rajesh Nishtala and others, NSDI 2013.
WhatsApp