← Back to blog

Scaling WordPress on a VPS for High-Traffic Websites

Maria Ilinca Bostan

WordPress scales well on a VPS once you stop letting PHP render pages it has already rendered. The order that matters is: page caching first, then object caching, then PHP-FPM and database tuning, then a CDN, and only then a bigger plan. Done in that order, a €10 instance comfortably serves traffic that an untuned €58 one struggles with.

Where WordPress time actually goes

An uncached WordPress request boots PHP, loads the plugins, runs dozens of database queries, renders a template and returns HTML — typically 200–800 ms of work to produce a page that has not changed since the last visitor asked for it. Caching removes that work; everything else on this page only makes the remaining work faster.

1. Page caching — the single largest win

Serve fully rendered HTML from disk or memory without touching PHP. A plugin such as WP Super Cache or W3 Total Cache does this, but the better version is caching at the proxy so requests never reach PHP-FPM at all:

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=wp:100m
                   inactive=60m max_size=1g use_temp_path=off;

set $skip_cache 0;
if ($request_method = POST)                     { set $skip_cache 1; }
if ($query_string != "")                        { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/cart|/checkout|/my-account") { set $skip_cache 1; }
if ($http_cookie ~* "comment_author|wordpress_logged_in|woocommerce_items_in_cart") { set $skip_cache 1; }

location ~ \.php$ {
    fastcgi_cache wp;
    fastcgi_cache_valid 200 60m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache     $skip_cache;
    add_header X-Cache $upstream_cache_status;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

The bypass conditions are the part to get right. Caching a logged-in user's page, or a WooCommerce cart, serves one customer another customer's session — a correctness bug, not a performance one. Verify with curl -I and read the X-Cache header.

2. Object caching with Redis

Page caching does nothing for logged-in users, the admin area or a shop checkout. Object caching does — it stores the results of repeated database queries in memory.

sudo apt install -y redis-server php-redis
# set maxmemory 256mb and maxmemory-policy allkeys-lru in /etc/redis/redis.conf
sudo systemctl restart redis-server php8.3-fpm

Then install a Redis object cache plugin and enable it. Setting maxmemory is mandatory — an unbounded Redis grows until the OOM killer intervenes, and the process it chooses is often PHP-FPM.

3. PHP-FPM sized to the machine

The default max_children is inherited from an assumption about a much larger server. Calculate it instead: (RAM available to PHP) ÷ (average process size). On an 8 GB Core instance with 2 GB for MySQL, 512 MB for Redis and 1 GB reserved, that leaves roughly 4.5 GB; at 60 MB per process that is about 75 children.

pm = dynamic
pm.max_children = 75
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 24
pm.max_requests = 500

pm.max_requests recycles workers periodically, which contains slow memory leaks in plugins you do not control. Setting max_children too high is worse than too low: a spike then pushes the machine into swap instead of forming a queue.

4. Database

-- the classic WordPress table bloat
DELETE FROM wp_options WHERE option_name LIKE '%_transient_%' AND autoload = 'no';
SELECT SUM(LENGTH(option_value))/1024/1024 AS autoload_mb
  FROM wp_options WHERE autoload = 'yes';

Autoloaded options are read on every single request. Sites that have accumulated years of plugins routinely carry several megabytes there, and trimming it is often the largest database win available. Then set innodb_buffer_pool_size to roughly half the memory you have allocated to MySQL, and enable the slow query log for a day to see what is actually costing you.

Post revisions are the other quiet consumer: define('WP_POST_REVISIONS', 5); in wp-config.php.

5. Media and delivery

Images are usually the majority of page weight. Serve WebP or AVIF, size them to their display dimensions, and lazy-load below the fold. Then put a CDN in front so static assets are served from the edge rather than from your instance. If you deliver a lot of video from the origin, a 10 Gbps port is the cheaper answer than a larger compute plan. Bandwidth and data transfer covers the sizing.

6. Only now, size the server

TrafficPlanNote
Under 30k visits/monthPulse — 4 GB, €6Comfortable with page caching on
30k–150k visits/monthCore — 8 GB, €10Room for Redis and a real buffer pool
WooCommerce, or 150k+Forge — 16 GB, €16Checkout traffic cannot be page-cached
Large shop or membership siteApex/Titan — 24–32 GBConsider splitting the database onto its own instance

WooCommerce and membership sites need more than a content site at the same visit count, because cart, checkout and account pages bypass page caching by necessity. Size for the uncached fraction, not the average.

Latency beats specification

A European audience served from a European site removes over a hundred milliseconds from every uncached request. Choose Frankfurt, Amsterdam or whichever of our twelve locations is closest to your readers before you consider a bigger plan — see choosing a datacenter location.

Frequently asked questions

How much traffic can WordPress handle on a VPS?

With full-page caching, a modest instance serves hundreds of thousands of cached page views a month without difficulty. Uncached, the same instance may struggle at a fraction of that. The caching strategy determines the answer far more than the plan does.

Do I still need a caching plugin if Nginx caches?

For page caching, no — proxy-level caching is faster because PHP never runs. You still want an object cache plugin for logged-in traffic.

Why is my admin area slow when the site is fast?

The admin area is never page-cached. That is object caching, PHP-FPM sizing and database health — the admin is the honest measure of your uncached performance.

Should the database go on a separate server?

Once the two are competing for memory on one box, yes. Keep them in the same location so the link between them is a fraction of a millisecond.

Next steps

Work through the general VPS performance guide for the layers below WordPress, and the migration guide if you are moving from shared hosting. Our WordPress hosting page has the plan comparison, and every plan gives you the root access these changes require.

Ready in under a minute

Deploy your first server now.

No contracts, no minimums. Start on an Ion KVM VPS at €0.0063 an hour and move to a monthly bare-metal server the day you outgrow it.

$ voxa deploy --plan ion --location amsterdam

KVM VPS billed hourly, capped monthly · Dedicated billed monthly · No setup fee

Included on every plan
Free IPv4 + IPv6
Every VPS
Unmetered traffic
1–10 Gbps
DDoS mitigation
2.5 Tbps
Root / IPMI access
Included
Setup fee
€0.00
Minimum term
None