Database Scaling Explained: Vertical vs Horizontal Scaling
By Oleksandr Andrushchenko — Published on
Database performance problems rarely appear because a database suddenly becomes slow. They emerge as data volume, concurrent connections, transaction rates, analytical queries, and background jobs gradually exceed the capacity of the original design.
Two primary strategies increase database capacity: vertical scaling, which makes one database server more powerful, and horizontal scaling, which distributes workload across multiple servers. Both can support production growth, but they solve different bottlenecks and introduce very different operational trade-offs.
Table of Contents
- Why Database Scaling Matters
- Identify the Real Bottleneck First
- Vertical Scaling
- Horizontal Scaling
- Vertical vs Horizontal Scaling
- Common Horizontal Scaling Patterns
- Read Replicas
- Database Sharding
- Distributed Databases
- Combining Both Strategies
- Capacity Planning
- Reliability and Failure Behaviour
- Migration Strategy
- Monitoring Database Scaling
- Common Mistakes
- Production Checklist
- Conclusion
- Key Takeaway
- More Articles to Read
Why Database Scaling Matters
A database must handle more than user-facing requests. Production systems also execute scheduled jobs, event consumers, reporting queries, data exports, schema migrations, backups, index maintenance, and replication.
As load grows, these activities compete for the same CPU, memory, storage, locks, and network bandwidth. The result is usually not a complete outage at first. It is increasing latency, unstable throughput, replication lag, timeout spikes, or reduced availability during peak traffic.
Typical warning signs include:
- CPU usage remains above 70–80% during sustained traffic.
- Database memory is insufficient to keep frequently used indexes and pages cached.
- Storage latency increases during writes, checkpoints, or compaction.
- Connection pools become exhausted.
- Lock waits and deadlocks increase.
- Replication lag grows during peak periods.
- Maintenance tasks take longer and interfere with application traffic.
- One tenant or query can degrade performance for the entire system.
Scaling should begin only after confirming that the database is actually the bottleneck. Poor indexes, inefficient queries, unbounded result sets, excessive connections, and badly designed transactions can make a large database instance perform worse than a smaller well-tuned one.
Identify the Real Bottleneck First
Scaling infrastructure without understanding the limiting resource often hides the problem temporarily. A CPU-bound workload requires a different solution from an I/O-bound workload, and neither is solved by adding memory alone.
| Observed Symptom | Likely Constraint | Useful Investigation |
|---|---|---|
| High CPU with low storage latency | Expensive query plans, sorting, joins, or insufficient compute | Inspect execution plans, slow-query logs, and CPU-heavy statements. |
| Low cache hit ratio | Insufficient memory or inefficient access patterns | Measure working-set size, index usage, and buffer-cache behaviour. |
| High disk latency | Storage saturation, random I/O, checkpoint pressure, or compaction | Measure IOPS, throughput, queue depth, WAL activity, and checkpoint duration. |
| Connection exhaustion | Too many application connections or long transactions | Inspect pool sizes, idle sessions, transaction duration, and connection churn. |
| High lock waits | Contention, oversized transactions, hot rows, or schema design | Inspect blocking queries, lock graphs, transaction scope, and write patterns. |
| Replica lag | Write volume, slow replay, network delay, or long-running replica queries | Measure WAL generation, replay speed, replica CPU, and network throughput. |
Rule of thumb: optimise the workload before changing the topology. Scaling an inefficient workload increases cost and operational complexity while preserving the original inefficiency.
Vertical Scaling
Vertical scaling, also called scaling up, increases the resources of one database server. The server receives more CPU cores, more memory, faster storage, higher IOPS, or additional network bandwidth.
The application continues using the same database endpoint and schema. No data-routing layer is required, and transactional behaviour remains mostly unchanged.
How Vertical Scaling Works
Database operations consume several hardware resources:
- CPU executes query plans, joins, sorts, compression, encryption, and background maintenance.
- Memory caches indexes and data pages, reducing storage reads.
- Storage persists table data, transaction logs, temporary files, and indexes.
- Network transports queries, results, replication traffic, and backups.
A vertically scaled architecture remains simple:
Clients
|
Application Instances
|
Connection Pool
|
Primary Database
- More CPU
- More RAM
- Faster storage
- Higher network bandwidth
Scaling may require an instance restart, a failover to a larger standby, or a maintenance window depending on the database platform.
Advantages
Simple architecture. Applications still communicate with one logical database endpoint. No shard routing, distributed query planning, or cross-node coordination is required.
Strong transactional semantics. Transactions remain local to one primary database, which simplifies locking, isolation, constraints, and rollback.
Minimal application changes. Existing SQL, ORMs, migrations, and reporting tools usually continue working without modification.
Lower operational overhead. Backup, restore, monitoring, schema changes, and incident response remain easier than in a distributed topology.
Fastest path to additional capacity. Increasing instance size can often provide immediate relief while longer-term architectural changes are evaluated.
Disadvantages
Finite hardware ceiling. Every instance family has a maximum CPU, memory, storage, and network capacity.
Non-linear cost. Very large instances are often disproportionately expensive compared with several smaller nodes.
Single write bottleneck. A larger primary can process more writes, but write throughput remains concentrated on one server.
Larger failure impact. When one large primary fails, a significant portion of the system may become unavailable until failover completes.
Maintenance risk. Instance upgrades, operating-system changes, and database upgrades may require restart or failover events.
When to Use Vertical Scaling
Vertical scaling is usually appropriate when:
- The database still fits comfortably within the capacity of larger available instances.
- Strong consistency and complex transactions are important.
- The workload is primarily OLTP.
- The team wants to minimise distributed-system complexity.
- Growth is predictable and does not require extreme write throughput.
- A near-term capacity problem must be solved quickly.
A single well-tuned PostgreSQL, MySQL, or SQL Server instance can support substantial production traffic. Horizontal scaling should not be introduced merely because the application has become important.
Production Example
Consider a PostgreSQL database running on 4 vCPUs, 16 GB RAM, and general-purpose SSD storage. Monitoring shows that frequently accessed indexes no longer fit in memory and CPU reaches 90% during peak traffic.
The instance is upgraded to 32 vCPUs, 128 GB RAM, and provisioned IOPS storage:
database:
engine: postgresql
version: "16"
instance_class: db.r7g.8xlarge
storage:
type: io2
size_gb: 2000
provisioned_iops: 30000
high_availability:
multi_zone: true
# Keep application connections bounded.
connection_pool:
max_connections: 300
The larger memory allocation improves cache hit rate, additional CPU supports more concurrent query execution, and faster storage reduces checkpoint and write latency.
This change may postpone architectural redesign for months or years, but it does not remove the need to monitor future growth.
Horizontal Scaling
Horizontal scaling, also called scaling out, adds more database nodes and distributes traffic, data, or both across them.
Horizontal scaling can increase read capacity, write capacity, storage capacity, and regional availability. However, the database is no longer one physical machine, so coordination, routing, consistency, and failure handling become part of the architecture.
How Horizontal Scaling Works
Horizontal scaling is not one specific technology. It is an architectural category that includes several patterns:
- Read replicas distribute read traffic while writes remain on one primary.
- Sharding distributes different subsets of data across independent database nodes.
- Distributed SQL spreads data across nodes while exposing a SQL interface and coordinated transactions.
- NoSQL partitioning distributes keys and replicas across a cluster.
A simplified horizontally scaled architecture looks like:
Application
|
Database Router
|
+----------------+----------------+
| | |
Node A Node B Node C
Data range 1 Data range 2 Data range 3
The routing layer may be implemented by the application, a proxy, a database driver, or the database platform itself.
Advantages
Higher aggregate capacity. Additional nodes can increase read throughput, write throughput, and storage capacity.
Reduced dependence on one machine. Data and traffic can be distributed across multiple failure domains.
Elastic growth. Some platforms allow nodes or partitions to be added as traffic grows.
Regional placement. Data can be replicated or partitioned closer to users, reducing latency and improving resilience.
Workload isolation. Reads, writes, analytical workloads, tenants, or regions can be separated onto different nodes.
Disadvantages
Distributed consistency. Replication lag, quorum behaviour, stale reads, and conflict resolution may affect application correctness.
Routing complexity. The system must determine which node owns each request or data record.
Cross-node operations. Joins, aggregates, constraints, and transactions become more expensive when data spans multiple nodes.
Operational overhead. Rebalancing, backup coordination, schema deployment, failover, and monitoring are more complex.
Uneven distribution. Poor partition keys can create hot shards that overload one node while others remain underused.
When to Use Horizontal Scaling
Horizontal scaling is appropriate when:
- A single server cannot provide enough storage or throughput.
- Read traffic can be separated from write traffic.
- Data can be partitioned by tenant, account, region, or another stable key.
- Global latency requirements justify regional replicas or partitions.
- Failure isolation is more important than architectural simplicity.
- Growth is expected to exceed the practical limits of vertical scaling.
Horizontal scaling should be introduced for a measured requirement, not as a default design preference.
Production Example
A multi-tenant SaaS platform stores customer events. One primary database can no longer sustain the write rate, and the largest available instance would only provide temporary relief.
The data is partitioned by account_id across four shards:
account_id hash % 4
0 -> shard-0
1 -> shard-1
2 -> shard-2
3 -> shard-3
Each account remains on one shard, so account-scoped transactions stay local. Aggregate reporting is moved to a separate analytics system instead of executing cross-shard queries against production databases.
Vertical vs Horizontal Scaling
Neither strategy is universally better. The correct choice depends on workload shape, growth rate, consistency requirements, operational maturity, and failure tolerance.
| Category | Vertical Scaling | Horizontal Scaling |
|---|---|---|
| Primary method | Add resources to one server | Add more servers |
| Application complexity | Low | Medium to high |
| Maximum capacity | Limited by the largest available machine | Can grow across many nodes |
| Transactional simplicity | High | Lower when transactions span nodes |
| Read scalability | Limited to one server unless replicas are added | High with replicas or distributed reads |
| Write scalability | Limited to one primary | High when writes are partitioned |
| Failure isolation | Lower | Potentially higher |
| Operational complexity | Lower | Higher |
| Cost model | Simple but expensive at the high end | More flexible but includes operational cost |
| Best fit | Moderate growth, strong transactions, simpler operations | Very large scale, global systems, partitionable workloads |
Practical sequence: optimise queries, scale vertically, add read replicas, isolate workloads, and introduce sharding only when measured limits justify it.
Common Horizontal Scaling Patterns
Horizontal scaling can distribute reads, writes, storage, or geographic traffic. Each pattern solves a different bottleneck.
| Pattern | Primary Benefit | Main Trade-Off |
|---|---|---|
| Read replicas | Higher read throughput | Replica lag and stale reads |
| Application-level sharding | Higher write and storage capacity | Routing and cross-shard complexity |
| Distributed SQL | Scale-out with SQL semantics | Coordination latency and operational complexity |
| Regional partitioning | Lower latency and data residency | Cross-region consistency and failover |
| Workload isolation | Protects OLTP traffic from analytics or batch jobs | Data movement and synchronization |
Read Replicas
Read replicas copy changes from a primary database and serve read-only traffic. Writes continue going to the primary, while reporting, search pages, dashboards, and other read-heavy endpoints can use replicas.
Application
|
+----------+----------+
| |
Writes Reads
| |
Primary +--------+--------+
| |
Replica A Replica B
The main trade-off is replication lag. A write committed on the primary may not immediately appear on a replica.
Read replicas work well when stale data is acceptable for a brief period. They are risky for flows that require read-after-write consistency, such as immediately loading a newly created order or verifying a just-updated balance.
Read Routing Example
The application can route consistency-sensitive reads to the primary and eventually consistent reads to replicas:
from enum import Enum
from typing import Protocol
class Consistency(Enum):
STRONG = "strong"
EVENTUAL = "eventual"
class DatabaseConnection(Protocol):
def execute(self, query: str, parameters: tuple = ()) -> list[dict]:
...
class DatabaseRouter:
def __init__(
self,
primary: DatabaseConnection,
replicas: list[DatabaseConnection],
) -> None:
if not replicas:
raise ValueError("At least one replica is required")
self.primary = primary
self.replicas = replicas
self._next_replica = 0
def connection_for_read(
self,
consistency: Consistency,
) -> DatabaseConnection:
# Read-after-write and correctness-sensitive queries use the primary.
if consistency is Consistency.STRONG:
return self.primary
# Round-robin routing is simple and avoids overloading one replica.
replica = self.replicas[self._next_replica]
self._next_replica = (self._next_replica + 1) % len(self.replicas)
return replica
Routing logic should also consider replica health and lag. A replica that is reachable but several minutes behind should not receive normal application traffic.
Database Sharding
Sharding divides data into independent subsets stored on different database servers. Each shard contains only part of the total dataset and processes writes for that subset.
Common shard keys include:
- Tenant or account ID
- Customer ID
- Geographic region
- Device ID
- Time range
- Hash of a stable identifier
The shard key determines data distribution, query routing, transaction boundaries, and future rebalancing cost. A poor shard key can make the architecture difficult to repair.
Shard Routing Example
Hash-based routing distributes account data across shards:
import hashlib
class ShardRouter:
def __init__(self, shard_names: list[str]) -> None:
if not shard_names:
raise ValueError("At least one shard is required")
self.shard_names = shard_names
def shard_for_account(self, account_id: str) -> str:
# A stable hash keeps the same account on the same shard.
digest = hashlib.sha256(account_id.encode("utf-8")).digest()
shard_number = int.from_bytes(digest[:8], byteorder="big")
return self.shard_names[shard_number % len(self.shard_names)]
router = ShardRouter([
"database-shard-0",
"database-shard-1",
"database-shard-2",
"database-shard-3",
])
target_shard = router.shard_for_account("account-84721")
Modulo-based routing is easy to understand but expensive to rebalance because adding a shard changes the mapping for many keys. Production systems often use consistent hashing, virtual shards, or an explicit shard map to reduce data movement.
Cross-shard queries should be avoided in latency-sensitive paths. Aggregations across all customers are better handled by a warehouse, search engine, or streaming analytics system.
Distributed Databases
Distributed databases spread data and replicas across multiple nodes while providing routing, replication, and failure handling inside the database platform.
Some distributed SQL systems support SQL queries and transactions across nodes. NoSQL systems often prioritise horizontal partitioning and predictable key-based access.
These platforms reduce application-level routing code, but they do not remove distributed-system trade-offs. Cross-node transactions require coordination, quorum decisions add latency, and network partitions affect availability or consistency.
| Characteristic | Single-Node Relational Database | Distributed Database |
|---|---|---|
| Transaction coordination | Local | May require multiple nodes |
| Scaling model | Primarily vertical | Primarily horizontal |
| Failure handling | Primary/standby failover | Replica groups, quorum, or consensus |
| Query flexibility | Strong for joins and ad hoc SQL | Varies by platform and partition layout |
| Operational complexity | Lower | Higher |
| Best use case | Transactional systems that fit on one primary | Large, distributed, or globally available workloads |
Combining Both Strategies
Most mature database architectures combine vertical and horizontal scaling.
A common progression is:
- Start with one primary database.
- Optimise queries, indexes, and connection management.
- Scale the primary vertically.
- Add a standby for high availability.
- Add read replicas for read-heavy traffic.
- Move analytical workloads to a separate system.
- Partition or shard only when the primary write path reaches a measured limit.
Even horizontally scaled systems often use large nodes. A sharded database may contain eight powerful shards rather than hundreds of small ones because fewer nodes reduce coordination and operational overhead.
Application
|
Connection / Routing
|
+---------------------+---------------------+
| |
Primary Shard A Primary Shard B
Large instance Large instance
| |
Read Replica A1 Read Replica B1
Read Replica A2 Read Replica B2
This architecture combines vertical capacity within each shard and horizontal capacity across shards and replicas.
Capacity Planning
Database scaling decisions should be based on trends, not emergencies. Capacity planning estimates when the current architecture will reach unsafe utilisation levels.
Important inputs include:
- Peak queries per second
- Peak transactions per second
- Read-to-write ratio
- Data growth per day
- Index growth
- Replication log generation
- Connection count
- CPU and storage utilisation
- Maintenance duration
- Recovery time requirements
Capacity should include operational headroom. Running a primary at 95% CPU may appear efficient, but it leaves no room for traffic spikes, failover, index creation, backups, or query-plan regressions.
Production target: maintain enough unused capacity to survive the loss of one node or a predictable peak event without immediate degradation.
| Metric | Warning Signal | Possible Response |
|---|---|---|
| CPU utilisation | Sustained above 70–80% | Tune queries, add compute, or redistribute workload. |
| Cache hit ratio | Declining as data grows | Add memory, reduce working set, or improve indexes. |
| Storage latency | Increasing during peak writes | Increase IOPS, tune checkpoints, or partition writes. |
| Replica lag | Exceeds application tolerance | Increase replica capacity, reduce replay load, or adjust routing. |
| Connection usage | Pool regularly reaches maximum | Reduce pool sizes, use a proxy, or shorten transactions. |
| Data growth | Storage or maintenance windows approach limits | Archive, partition, shard, or move historical data. |
Reliability and Failure Behaviour
Scaling changes how failures affect the system.
A vertically scaled primary has a simple failure model: the primary is available or unavailable. High availability usually depends on a standby and automated failover.
A horizontally scaled system can continue operating when one node fails, but only if routing, replication, and data placement support that behaviour. A failed shard may still make one portion of the data unavailable even while the rest of the system remains healthy.
| Failure | Vertical Architecture | Horizontal Architecture |
|---|---|---|
| Primary node failure | Requires standby promotion or restore | May affect one shard, replica group, or the entire cluster |
| Network partition | Usually isolates the primary or standby | Can trigger quorum loss, stale reads, or split-brain protection |
| Hot key or tenant | Degrades the shared primary | Can overload one shard while others remain healthy |
| Schema migration failure | Affects one database | Can leave shards on different schema versions |
| Backup failure | One backup pipeline is affected | One or more shard backups may be incomplete |
Horizontal scaling improves failure isolation only when failures are intentionally contained. Independent shards, bounded retries, health-aware routing, and tested recovery procedures are required.
Migration Strategy
Moving from one primary database to a distributed design should be incremental. A full rewrite introduces unnecessary risk.
A safer sequence is:
- Measure workload patterns. Identify high-volume tables, read-heavy endpoints, hot tenants, and transaction boundaries.
- Separate reads. Add replicas and route tolerant workloads first.
- Isolate analytics. Move reporting and large scans away from the transactional primary.
- Introduce a routing abstraction. Centralise database-selection logic before adding shards.
- Choose a stable partition key. Prefer a key aligned with application ownership and transaction boundaries.
- Create virtual shards. Map many logical partitions to fewer physical databases for easier rebalancing.
- Backfill data safely. Copy existing records while validating counts, checksums, and referential consistency.
- Use dual writes carefully. Make writes idempotent and reconcile failures instead of assuming both destinations always succeed.
- Move traffic gradually. Migrate tenants or key ranges in controlled batches.
- Retain rollback paths. Keep the original source authoritative until validation is complete.
A routing table can decouple application identifiers from physical database locations:
CREATE TABLE tenant_shard_map (
tenant_id UUID PRIMARY KEY,
shard_name VARCHAR(100) NOT NULL,
migration_state VARCHAR(30) NOT NULL DEFAULT 'active',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_tenant_shard_map_shard
ON tenant_shard_map (shard_name);
This design allows one tenant to move between shards without changing its identifier.
Monitoring Database Scaling
Monitoring must reflect the chosen topology. A horizontally scaled database can appear healthy at the cluster average while one shard is overloaded.
Track metrics at both the cluster and node level:
- CPU, memory, storage latency, and network throughput per node
- Queries and transactions per second
- Slow queries and execution-plan changes
- Connection-pool utilisation
- Lock waits and deadlocks
- Replication lag per replica
- Data size and growth per shard
- Traffic distribution per shard key
- Rebalancing progress
- Backup age and restore-test results
- Failover duration
- Error rates by database node
Do not rely only on averages. Percentiles, per-node dashboards, and per-tenant metrics reveal hot spots hidden by aggregate values.
Common Mistakes
| Mistake | Why It Causes Problems | Better Approach |
|---|---|---|
| Scaling before optimising queries | Additional hardware hides inefficient scans, joins, and indexes while cost continues increasing. | Use execution plans and slow-query data to remove the largest sources of wasted work first. |
| Introducing sharding too early | Routing, migrations, transactions, backups, and reporting become distributed before scale requires it. | Scale vertically and add replicas until measured write or storage limits justify sharding. |
| Choosing a low-cardinality shard key | A few partitions become hot and prevent even distribution of data and traffic. | Select a stable, high-cardinality key aligned with access patterns and transaction boundaries. |
| Sending every read to replicas | Replication lag can return stale data and break read-after-write flows. | Classify reads by consistency requirement and route strong-consistency reads to the primary. |
| Ignoring cross-shard transactions | Operations that were atomic on one database become slow, fragile, or only partially completed. | Keep related data on the same shard or redesign workflows around idempotent asynchronous coordination. |
| Using modulo sharding without a migration plan | Adding or removing a shard remaps large portions of the dataset. | Use virtual shards, consistent hashing, or an explicit shard map. |
| Monitoring only cluster averages | One overloaded shard or replica can be hidden by underutilised nodes. | Alert on per-node, per-shard, and per-tenant saturation metrics. |
| Scaling connection pools with application instances | Each new application instance opens more database sessions until the database is exhausted. | Set a global connection budget and use a connection proxy or pooler. |
| Assuming more nodes automatically improve availability | Shared routing, quorum, schema, or network dependencies can still fail together. | Test real failure domains and verify that traffic continues during node and zone outages. |
| Skipping restore and rebalancing tests | Backups or shard-movement procedures may fail only during an incident. | Run scheduled restore drills and practise moving data between nodes under load. |
Production Checklist
- Measure the bottleneck. Confirm whether CPU, memory, storage, locks, connections, or replication is limiting throughput.
- Review slow queries. Fix the highest-cost queries before increasing infrastructure.
- Validate indexes. Remove unused indexes and add indexes that match real filtering, joining, and ordering patterns.
- Define a connection budget. Limit total database connections across all application instances and workers.
- Maintain headroom. Reserve enough capacity for traffic spikes, failover, backups, and maintenance.
- Test vertical upgrades. Measure restart, failover, and performance behaviour before resizing production.
- Classify reads by consistency. Document which endpoints may use replicas and which require the primary.
- Monitor replica lag. Remove lagging replicas from normal read routing automatically.
- Choose shard keys from access patterns. Align data placement with tenant ownership, transaction boundaries, and query routing.
- Plan for hot tenants. Support dedicated shards, tenant movement, or rate limiting for disproportionate workloads.
- Use virtual shards. Separate logical partitioning from physical database nodes to simplify rebalancing.
- Avoid cross-shard joins. Move global aggregates and analytical queries to systems designed for distributed reporting.
- Automate schema deployment. Roll out backward-compatible migrations across every shard and verify schema versions.
- Test backups per node. Confirm that every shard and replica group is included in backup and restore procedures.
- Practise failover. Measure recovery time and application behaviour during primary, replica, shard, and zone failures.
- Track data distribution. Alert when shard size, traffic, or write rate becomes unbalanced.
- Design idempotent migrations. Make backfills and dual-write processes safe to retry.
- Keep rollback paths. Preserve the previous data path until migrated traffic and data integrity are verified.
Conclusion
Vertical scaling and horizontal scaling are complementary strategies rather than competing answers.
Vertical scaling is simpler, preserves local transactions, and is often the best first response to growth. Horizontal scaling increases aggregate capacity and failure isolation, but introduces routing, replication, consistency, and operational complexity.
The most reliable production path is incremental: optimise the workload, scale the primary vertically, add read replicas, isolate non-transactional workloads, and introduce sharding or a distributed database only when measured requirements justify the change.
Key Takeaway
Use the simplest database topology that meets current reliability and capacity requirements. Scale vertically while one primary remains practical, scale reads with replicas, and scale writes horizontally only after the workload, partition key, transaction boundaries, and operational model are clearly understood.
More Articles to Read
- Replication and Read Replicas in Distributed Databases
- Database Sharding Strategies and Trade-Offs
- Designing High-Performance Database Schemas
- Partitioning Large Tables for Production Systems
- SQL vs NoSQL: Choosing the Right Database
- Database Best Practices for Scalable Applications
Comments (0)