Patterns

Scaling Writes

The harder half of scaling — you can't just copy your way out of write load. Buffering with queues, batching, sharding the write path, and picking stores built for heavy writes.


Reads are easy to scale because you can make endless copies. Writes are harder: every copy you made for reads is now something a write has to update, and a single primary can only absorb so many writes per second. This pattern is the set of moves for when writes — not reads — are the pressure.

First, a reality check: one modern database handles thousands of writes per second. Most systems never outgrow that. Reach for the heavy tools only when your estimate genuinely exceeds it.

1. Buffer bursts with a queue

If writes arrive in spikes (a flash sale, a viral post) faster than the database can commit them, don't make users wait and don't drop them — put a queue between the request and the database. The endpoint enqueues the write and responds immediately; workers drain the queue into the database at a steady rate the DB can sustain.

spiky traffic ──► [ queue ] ──steady──► [ DB ]  (workers pull at a safe rate)

This smooths a 10×-for-10-seconds spike into a sustainable flow. The trade is that the write is now asynchronous — "accepted" doesn't mean "committed yet," so the UI must say "processing" rather than "done."

2. Batch many small writes into few

Committing 1,000 rows one at a time is far slower than one bulk insert, because the per-write overhead (round trip, transaction, index update) dominates. Workers that pull from the queue can batch: collect writes for a short window or up to a count, then write them in one statement. This is how high-volume ingestion (analytics events, metrics, clicks) keeps up — it trades a little latency for a huge throughput gain.

3. Shard the write path

If the write volume truly exceeds one primary, split the data across multiple primaries by a shard key, so each one takes a fraction of the writes. This is the big hammer — see Sharding. The key must spread writes evenly: sharding event data by timestamp creates a single hot shard (all new writes land on "today"); sharding by user_id or tenant_id spreads them. Cross-shard transactions mostly go away, so design each write to touch one shard.

4. Use a store built for writes

Relational databases optimise for correctness and flexible reads. Some stores are built specifically to absorb enormous write volume:

  • Wide-column (Cassandra, DynamoDB): append-friendly, horizontally scalable writes, tunable consistency. Great for time-series, event logs, feeds.
  • Log/stream systems (Kafka): take writes at firehose rates by being an append-only log, then let consumers process at their own pace.

The pattern is often: Kafka absorbs the firehose → stream processors aggregate → results land in a serving store. The database you read from never sees the raw write rate.

5. Pre-aggregate instead of storing every write

Sometimes the cheapest write is the one you don't make. If you only ever need counts, increment a counter (in Redis or a counter table) instead of inserting a row per event. Ten thousand "likes" become ten thousand increments to one number, not ten thousand rows — and the read is already computed.

Choosing

PressureReach for
Spiky writes, users shouldn't waitQueue + async workers
Many tiny writesBatch them
Sustained volume past one primaryShard by an even key
Firehose ingestionKafka + stream processing
Only need aggregatesCounters / pre-aggregation

What to say in an interview

Separate the spike from the sustained rate, and buffer before you shard:

"The ad-click stream is write-heavy and bursty. I wouldn't write each click to Postgres — I'd push clicks into Kafka to absorb the firehose, batch-aggregate them per minute with a stream processor, and store the rollups in a store I can read cheaply. If I needed raw events too, I'd put them in a wide-column store sharded by a key that spreads evenly, not by time."


Next up: Handling Large Blobs — keeping big files out of your database and request path.