Scaling up a monolith vertically means adding more resources to a single, existing server instead of spreading the load across multiple servers.

Let’s see this in action. Imagine a simple Python web application running on a single server.

from flask import Flask
import time
import threading

app = Flask(__name__)
requests_processed = 0
lock = threading.Lock()

@app.route('/')
def index():
    global requests_processed
    with lock:
        requests_processed += 1
    # Simulate some CPU-bound work
    start_time = time.time()
    while time.time() - start_time < 0.01:
        pass # Busy wait
    return f"Hello! Processed {requests_processed} requests.\n"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

If this server has limited CPU and RAM, it can only handle so many requests concurrently. We can monitor its performance.

# On the server, using htop
# Observe CPU usage (%) and Memory usage (MiB)
htop

When the CPU hits 100% or RAM is exhausted, the application slows down dramatically, and new requests might time out.

The problem this solves is the immediate need for more capacity when a single instance is bottlenecked. Instead of the complex architectural shift of microservices or distributed systems, vertical scaling is the pragmatic first step. It leverages the existing, simpler architecture.

Internally, when you scale vertically, you’re essentially telling the operating system and the hardware to provide more "power" to the process running your application. This means:

  • More CPU Cores: The OS can schedule more threads or processes to run in parallel on the CPU. For our Python app, which is often CPU-bound due to the Global Interpreter Lock (GIL), more cores allow more independent Python processes (if using something like Gunicorn or uWSGI) to run, or at least allow the OS to manage I/O more efficiently.
  • More RAM: The application can load more data into memory, hold more connections open without swapping to disk (which is orders of magnitude slower), and avoid running out of memory, which would lead to crashes or severe performance degradation.
  • Faster I/O: More powerful disks or network interfaces mean the server can read/write data and communicate over the network more quickly, reducing bottlenecks in data retrieval or external API calls.

The exact levers you control are the instance types or hardware specifications. For a cloud VM, this means choosing a larger instance size (e.g., moving from t3.medium to t3.xlarge on AWS). For physical hardware, it means adding more RAM modules, upgrading the CPU, or replacing the hard drive with an SSD.

The most surprising true thing about vertical scaling is that it often doesn’t require any application code changes whatsoever. If your monolith is well-behaved and doesn’t have fundamental architectural flaws like infinite loops or unmanaged memory growth, simply giving it more resources can often be the most cost-effective and simplest way to handle increased load for a significant period. It buys you time to plan more complex architectural evolutions.

To scale vertically, you typically stop your application, provision a new, more powerful server (or resize the existing VM), and then restart your application on that new hardware. For a cloud VM, this might look like:

  1. Stop the instance: aws ec2 stop-instances --instance-ids i-0123456789abcdef0
  2. Modify the instance type: aws ec2 modify-instance-attribute --instance-id i-0123456789abcdef0 --instance-type '{"Value": "t3.xlarge"}'
  3. Start the instance: aws ec2 start-instances --instance-ids i-0123456789abcdef0
  4. Re-attach any EBS volumes, update security groups, etc., as needed.
  5. Verify the application is running on the new hardware.

The next step after hitting the limits of vertical scaling is often horizontal scaling, where you introduce load balancing and multiple instances of your application.

Want structured learning?

Take the full Monolith course →