When your application outgrows its server, you have two basic options: make the server bigger (vertical scaling), or add more servers (horizontal scaling). Most real-world growth uses both, in a sensible order. Here’s how each works, what it costs in complexity, and how to plan it.
Vertical scaling (scale up)
Move to a plan with more vCPU, RAM or disk.
Pros
- No application changes needed.
- Simplest to operate: one server, one place to look.
- Great for databases, which are hard to distribute.
Cons
- There’s an upper limit (the largest plan or server available).
- A single point of failure remains.
- Upgrades may involve a short downtime window.
On VPS.DO, plan upgrades are handled through a support ticket. See when to upgrade your VPS.
Horizontal scaling (scale out)
Run several servers that share the load.
Pros
- Nearly unlimited capacity, by adding nodes.
- Redundancy: one server can fail without taking the service down.
- Rolling deployments with zero downtime.
Cons
- The application must be designed for it (stateless app servers, shared storage and sessions).
- More moving parts: load balancers, networking, configuration management, monitoring.
Step 1 (usually): split roles
Before running multiple identical app servers, separate different roles onto different servers:
┌───────────── app server (Nginx + PHP/Node/Python)
users ──► │
└───────────── database server (MySQL/PostgreSQL) ── backups → storage server
- The database gets dedicated RAM for its buffer pool and cache.
- The app server gets all its CPU for requests.
- Connect them over a private network or a WireGuard tunnel, and never expose the database publicly. See WireGuard.
Step 2: make the app stateless
To run several app servers, nothing important can live only on one of them:
| State | Move it to |
|---|---|
| Sessions | Redis, or the database. See install Redis |
| User uploads | Object storage (S3/MinIO), or shared storage. See MinIO |
| Cache | Redis/Memcached (shared) |
| Scheduled jobs | One designated worker, or a distributed lock |
| Config and code | Git + CI/CD, or Ansible, so every server is identical |
Step 3: load balancing
Put a load balancer in front of two or more app servers:
upstream app {
least_conn;
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
}
server {
listen 443 ssl;
server_name example.com;
location / { proxy_pass http://app; }
}
See Nginx load balancing, HAProxy, and load balancer types.
The load balancer itself becomes a single point of failure. Use two, with a floating IP or keepalived, or DNS-level failover. See VPS high availability.
Step 4: scale the database
Databases scale differently:
- Vertical first: more RAM and faster disk go a long way.
- Read replicas for read-heavy workloads. See database replication explained.
- Caching to avoid queries entirely (object cache, query cache in the app).
- Sharding only at very large scale. It adds a lot of complexity. See database sharding.
Caching beats scaling
Before adding servers, make each request cheaper:
- full-page or HTTP caching (Nginx FastCGI cache, Varnish, CDN);
- an object cache (Redis);
- background jobs for slow work (emails, exports, image processing).
A cached page costs a tiny fraction of an uncached one. See Nginx FastCGI cache and CDN for websites.
A practical growth roadmap
| Stage | Setup |
|---|---|
| 1 | One VPS, with caching and monitoring |
| 2 | Bigger VPS (vertical) |
| 3 | Split: app server + database server (+ backup/storage server) |
| 4 | Stateless app, 2+ app servers behind a load balancer |
| 5 | Database replicas, separate workers, CDN, multiple regions |
Measure before each step, and only add complexity when the numbers say so.
Quick reference
| Vertical | Horizontal | |
|---|---|---|
| How | Bigger plan | More servers |
| App changes | None | Stateless design |
| Redundancy | No | Yes |
| Limit | Largest server | Practically none |
| Best for | Databases, early growth | Web/app tiers, high availability |
Related: VPS for a SaaS startup and VPS vs cloud hosting.