August 28, 2026
Read Replicas, Sharding, and Scheduled Jobs: Three Ways Database Load Actually Gets Managed
Three distinct techniques get reached for when a database is under too much load, and they're easy to confuse for each other. Here's what each one actually does, and why they solve different problems.
Once a database's load has been diagnosed as a genuine capacity problem rather than a query that just needs fixing, three distinct techniques commonly get reached for: read replicas, sharding, and scheduling heavy work outside of live traffic entirely. They're easy to confuse for each other, since all three are described loosely as "scaling the database," but they solve genuinely different problems and aren't interchangeable.
Read replicas and read/write splitting
The standard pattern for scaling read traffic is primary-replica replication: one or more replica servers continuously receive a stream of changes from a primary server and serve read-only queries, while all writes go to the primary. PostgreSQL's own documentation, describing this under the name "Hot Standby," states that a standby server can be used for read-only queries while it continues replicating.
Routing reads to a replica and writes to the primary is commonly called read/write splitting, though it's worth noting there isn't one single universally standardized term for this. ProxySQL, the most widely used tool for this in the MySQL ecosystem, documents the feature literally as "read/write split" in its own materials, describing hostgroup-based routing where writes go to the primary and reads are distributed across replicas via configurable query rules. PostgreSQL has no first-party built-in equivalent; this routing is typically handled by the application itself, an external proxy, or cloud-vendor tooling such as separate reader and writer endpoints.
The real tradeoff, and the one that has to be understood honestly before adopting this pattern: replication is asynchronous by default, so replicas lag behind the primary. A read immediately following a write can be served from a replica that hasn't yet applied that write, producing a "read-your-own-writes" inconsistency that any read/write-splitting setup has to account for, whether by routing certain reads back to the primary or by accepting the eventual consistency tradeoff for that use case.
Sharding: a genuinely different technique
Read replicas copy the same, full dataset to additional servers to scale read throughput. Sharding does something structurally different: it splits the data itself, by some key, across separate database instances. AWS's own database engineering blog draws this distinction directly, noting that read replicas mainly address read pressure, while sharding addresses write, storage, and horizontal scale limits, at a meaningfully higher operational cost.
This distinction is worth holding onto precisely because the two get confused often: adding a read replica doesn't help if a database is running out of write capacity or storage space, since every replica still holds the full dataset and none of them can absorb additional writes. Sharding is the tool for that problem, at the cost of real complexity: queries that need data from more than one shard, and application logic that has to know which shard holds which data. A smaller, often separately confused technique is table-level partitioning, splitting one logical table into physical pieces within the same database instance, which addresses query performance on a large table rather than distributing load across multiple machines at all.
Scheduled jobs as a structurally different load pattern
Live, user-facing request traffic is, ideally, many short, independent operations arriving at essentially random times. Scheduled and background jobs are close to the inverse: bursty, clustered around predictable times, and often a small number of long-running, resource-intensive operations rather than many small ones.
pg_cron, the standard Postgres-native scheduler, runs as a background worker tracking jobs in a table and executing them on cron syntax. Its own documentation specifies a concurrency detail worth knowing for capacity planning: pg_cron can run many different jobs in parallel, but never runs two instances of the same job at once. If a job is still running when its next scheduled time arrives, the new run queues and starts once the current one finishes. Celery, representing the general distributed-task-queue pattern outside Postgres specifically, documents its own core rationale plainly: moving work off request-handling workers into asynchronous queued tasks keeps those workers free to respond to live requests quickly, rather than tying one up for the duration of a long operation.
The reason this is a distinct load pattern worth planning for separately, not folding it into "traffic" generally, is that a nightly batch job spiking database load at 2 a.m., holding locks longer, running large scans, growing replication lag, won't show up in live-traffic monitoring at all, since nothing about live request volume changed. Capacity planning has to treat scheduled load as its own overlapping curve, not assume it's captured by whatever dashboard tracks request throughput.
Applying it
- Reach for a read replica specifically when read query volume is the bottleneck, and design for the replication lag it introduces rather than being surprised by it later.
- Don't expect a read replica to help with a write-capacity or storage problem; that's what sharding addresses, at real added complexity, and it's worth confirming which problem actually exists before taking on that complexity.
- Treat scheduled and background jobs as their own load pattern with their own capacity requirements, not as something automatically covered by monitoring built around live request traffic.
- Check a scheduler's own concurrency guarantees (whether it prevents overlapping runs of the same job, how it queues a delayed run) before assuming a long-running job won't stack up on itself under load.