Scaling Writes
Diagnose write bottlenecks and apply partitioning, batching, async fan-out, and conflict control without pretending every workload is read-heavy.
Most scalability folklore starts with caches and read replicas. Writes are harder: every durable mutation must land somewhere, contend for locks or versions, and often invalidate cached views. Scaling writes means reducing contention, spreading load, and accepting explicit consistency trade-offs—not only adding CPUs.
Where writes hurt
Typical write bottlenecks:
| Bottleneck | Symptom | First levers |
|---|---|---|
| Single primary CPU/IO | Saturating disk or WAL | Vertical scale, batching, faster storage |
| Hot row / hot partition | Lock waits, retries | Key redesign, queues, sharding |
| Secondary indexes | Write amplification | Fewer indexes, deferred indexing |
| Synchronous fan-out | Latency spikes | Async events, outbox |
| Cross-entity transactions | Long critical sections | Smaller transactions, sagas |
- Write amplification
Extra I/O or work caused by a logical write—indexes, replicas, caches, audit logs, and compacted storage formats all multiply cost.
Measure before reshaping
Instrument:
- Writes per second and p99 latency by endpoint/aggregate
- Lock wait / deadlock rates
- WAL or redo volume
- Hot keys (top entities by mutation rate)
Without hot-key data, you may shard a cold table and leave the real fire untouched.
Vertical scale and batching
Before distribution:
- Batch inserts/updates — fewer round-trips and fsync boundaries
- Bulk APIs — amortize planning and index maintenance
- Disable chatty ORM patterns — N+1 updates become one statement
- Right-size transactions — hold locks for microseconds of work, not remote calls
// Prefer set-based updates over per-row chatty loops
await db.ExecuteAsync(
"""
UPDATE inventory
SET quantity = quantity - @qty
WHERE sku = @sku AND quantity >= @qty
""",
new { sku, qty });Partitioning write load
When one node cannot absorb write QPS or dataset size, partition:
- Choose a shard key with even write distribution (avoid monotonic ids alone if they create hot tails)
- Route writes to the owning shard
- Accept that cross-shard transactions are expensive or unavailable
Placement often uses consistent hashing or range schemes described in database sharding.
Decouple with asynchronous work
Not every side effect must complete in the request path:
- Persist the authoritative write
- Publish an event (event-driven architecture)
- Let projections, search indexes, and notifications catch up
This improves request latency and smooths spikes, at the cost of eventual consistency for derived views.
await SaveOrderAsync(order, ct);await UpdateSearchIndexAsync(order, ct);await SendEmailAsync(order, ct);await WriteAnalyticsAsync(order, ct);// Caller waits for all side effectsawait SaveOrderWithOutboxAsync(order, ct);// Search, email, analytics consume OrderPlacedreturn Accepted(order.Id);Contention and concurrency control
Hot entities need explicit strategies:
- Serialize writers through a per-key queue
- Optimistic concurrency with version columns—retry on conflict
- Pessimistic locks for short, high-conflict critical sections
See pessimistic vs optimistic locking for the trade-offs. Blind retries without backoff can amplify load during incidents.
CQRS-shaped write models
Separating write models from read models lets you:
- Optimise the write path for invariants and durability
- Scale reads independently with denormalised projections
- Avoid forcing every query shape onto the transactional store
This is a natural companion to modular boundaries in a modular monolith: the write module owns commands; read modules own queries.
What not to do
- Cache your way out of writes — caches help reads; write-through still hits the store
- Shard without a key strategy — random sharding destroys locality for related entities
- Multi-master everywhere — conflict resolution becomes the product
- Ignore idempotency — retries under load will double-apply mutations