Scalability & Performance Scalability

Vertical Scaling

Increasing CPU/RAM of a single instance; when vertical beats horizontal, NUMA architecture, database vertical scaling, diminishing returns, downtime requirements, and cost model.

⏱ 10 min read

What it is

Vertical scaling (also called "scaling up") means increasing the resources of an existing machine: more CPU cores, more RAM, faster storage (NVMe vs. SATA SSD), or higher network bandwidth. Instead of running 10 × 4-vCPU instances, you run 1 × 40-vCPU instance. The software architecture does not need to change for vertical scaling — the same binary runs on a bigger box and benefits from more resources without any code modifications.

In cloud environments, vertical scaling means moving from a smaller instance type to a larger one (e.g., AWS RDS db.r6g.2xlarge → db.r6g.8xlarge). On bare-metal systems it may mean adding physical DIMM slots or swapping CPU packages. Modern cloud providers support "live resize" for some resources (EBS volume expansion, RDS storage autoscaling), but resizing compute typically requires a restart, making vertical scaling a planned operation rather than an elastic one.

Why it exists

Not all workloads parallelize well. A single-threaded legacy application, a relational database write primary, or an in-memory analytics engine with complex cross-record joins may extract little benefit from a fleet of small instances. For these cases, vertical scaling is the only practical path to higher throughput. It is also operationally simpler: there is no load balancer to configure, no distributed session management, and no coordination overhead — one machine handles everything.

Databases are the canonical vertical scaling use case. PostgreSQL, MySQL, and Oracle are all optimized to exploit large shared_buffers, more CPU parallelism for query execution, and faster I/O. Doubling the RAM on a database server can eliminate almost all disk I/O for a working set that previously only half-fit in cache. The total cost of ownership often favors one very large RDS instance over the complexity of running a distributed database cluster with sharding, cross-shard query routers, and resharding operations.

When to use

  • The bottleneck is a stateful component that cannot be horizontally scaled without significant architectural change (database write primary, single-leader Kafka broker, ZooKeeper ensemble member).
  • Your application is stateful with large in-memory working sets (in-memory caches, analytics engines like ClickHouse or DuckDB) that benefit greatly from more RAM.
  • You need a quick, low-risk capacity increase with minimal code change — vertical scaling is often the fastest path to relief during an incident.
  • Licensing costs or complexity of distributed coordination makes a single large instance preferable to many small ones.
  • CPU-bound workloads with high thread contention may benefit from NUMA-aware scheduling on a single large machine rather than cross-network communication in a cluster.
  • The current instance type is demonstrably under-provisioned and a cost/performance analysis shows a larger type delivers better price-performance than the next horizontal tier.

When not to use

  • You have already reached the largest available instance type and throughput is still insufficient — there is no bigger box to move to.
  • Traffic patterns are spiky and unpredictable; a large always-on instance is cost-inefficient compared to auto-scaled horizontal fleets.
  • High availability is critical and you cannot afford even the brief downtime that most vertical resize operations require.
  • The workload is embarrassingly parallel and stateless — horizontal scaling will give you better throughput per dollar and better fault tolerance.

Typical architecture


  Database Vertical Scaling — Before and After

  BEFORE (under-provisioned):
  ┌──────────────────────────┐
  │  db.r6g.xlarge           │
  │  4 vCPU  │  32 GB RAM    │
  │  shared_buffers: 8 GB    │
  │  Working set: 25 GB      │  ← 17 GB spills to disk!
  │  IOPS: 3,000             │
  └──────────────────────────┘
   Symptoms: high disk read latency, CPU wait spikes

  AFTER (vertically scaled):
  ┌──────────────────────────┐
  │  db.r6g.4xlarge          │
  │  16 vCPU │  128 GB RAM   │
  │  shared_buffers: 48 GB   │
  │  Working set: 25 GB      │  ← fully in memory
  │  IOPS: 12,000            │
  └──────────────────────────┘
   Result: near-zero disk reads, CPU-bound query execution

  NUMA Topology (2-socket server):
  ┌────────────┐    ┌────────────┐
  │  NUMA Node 0│    │  NUMA Node 1│
  │  24 cores   │    │  24 cores   │
  │  256 GB RAM │    │  256 GB RAM │
  │  Local NVMe │    │  Local NVMe │
  └──────┬──────┘    └──────┬──────┘
         └─────QPI/xGMI─────┘
   Cross-NUMA access ~2× slower than local
          

Pros and cons

Pros

  • No application code changes required; the same binary benefits automatically.
  • No distributed coordination, load balancers, or session externalization needed.
  • Lower latency for workloads with large shared state (all data fits in a single memory space).
  • Operationally simple — fewer moving parts, easier to monitor and debug.
  • Effective for database workloads where cache hit ratio improvements are dramatic.

Cons

  • Hard ceiling: there is a largest available instance type, and you will eventually hit it.
  • Typically requires downtime to resize compute (though some cloud services minimize this).
  • Diminishing returns: doubling CPU rarely doubles throughput beyond a certain point due to lock contention and memory bandwidth limits.
  • Single point of failure: one big instance means one failure domain; high availability requires expensive standby replicas.
  • Cost inefficiency during low-traffic periods: a large instance runs 24/7 regardless of demand.

Implementation notes

Diminishing returns and Amdahl's Law: Vertical scaling follows Amdahl's Law — if 20% of your workload is serial and non-parallelizable, the maximum theoretical speedup is capped at 5× regardless of how many cores you add. Profile your application first to identify the actual bottleneck before resizing. For databases, use pg_stat_statements, EXPLAIN ANALYZE, and slow query logs to confirm whether you are CPU-bound, memory-bound (cache miss rate), or I/O-bound before choosing the instance upgrade direction.

NUMA considerations: On large multi-socket servers (96+ cores), Non-Uniform Memory Access topology matters. A PostgreSQL instance will perform differently depending on whether its memory allocations are NUMA-local or must cross the interconnect. Use numactl --hardware to inspect topology and pin the database process to a NUMA node with numactl --cpunodebind=0 --membind=0 to prevent cross-NUMA latency. In cloud environments, "bare metal" instance types (e.g., i4i.metal on AWS) expose NUMA directly, while virtualized instances abstract it away. Cloud resize procedure: for RDS, initiate an instance class modification during a maintenance window; Multi-AZ deployments will perform a failover to the standby, minimizing disruption to typically under 60 seconds.

Common failure modes

  • Upgrade to wrong bottleneck dimension: Adding CPU when the real bottleneck is memory bandwidth, or adding RAM when you're I/O-bound. Always profile first.
  • License cost explosion: Some database engines (Oracle, SQL Server) charge per-core; doubling cores doubles licensing costs, quickly exceeding hardware savings.
  • Lock contention at higher concurrency: A larger instance allows more parallel threads, which can increase contention on global mutexes (e.g., PostgreSQL's lock manager, InnoDB buffer pool mutex), causing performance to plateau or regress.
  • Memory fragmentation: Long-running processes on large memory instances can suffer heap fragmentation; Java applications benefit from explicit GC tuning (G1GC heap region sizing) at large heap sizes.
  • Standby lag during failover: When failing over a Multi-AZ database after a resize, the standby may be temporarily behind in replication, causing brief write unavailability.

Decision checklist

  • Profiling has identified the specific resource bottleneck (CPU, RAM, I/O, network) rather than assuming.
  • The cost of the target instance type is compared against horizontal scaling alternatives including operational overhead.
  • A maintenance window or failover procedure is planned to minimize user-visible downtime during the resize.
  • Application configuration (connection pool sizes, thread pool sizes, JVM heap) will be updated to take advantage of new resource limits.
  • A rollback plan exists if the larger instance type does not yield expected improvement (e.g., performance regression from lock contention).
  • High availability implications are reviewed: does a Multi-AZ or standby replica need to be resized simultaneously?

Example use cases

  • PostgreSQL write primary: An OLTP database serving 5,000 writes/second is consistently hitting 80% CPU on db.r6g.2xlarge. Upgrading to db.r6g.4xlarge with double the cores and RAM reduces CPU to 35% and eliminates the slow-query backlog.
  • In-memory analytics: A ClickHouse node processing real-time dashboards benefits from moving to a 192 GB RAM instance, allowing the entire working dataset to reside in memory and reducing query latency by 10×.
  • Legacy monolith: A single-threaded legacy Java application cannot be easily refactored to run in a distributed fashion; upgrading to a faster single-core CPU instance (higher clock speed) provides the most effective improvement.
  • Horizontal Scaling — the complementary pattern; combine both for cost-efficient scalability.
  • Read Replicas — a form of horizontal scaling specifically for database read traffic, enabling the write primary to remain vertically scaled.
  • Connection Pooling — larger instances support more concurrent connections; pooling prevents exhaustion when the instance grows.

Further reading