DocsPerformance and capacity

Performance and capacity

How ZoPanel fits hundreds of sites on a small VPS: a memory plan, PHP that sleeps when idle, page cache, memory compression and load shedding.

ZoPanel is built to host many websites on a small server. It sizes itself to the machine, stops PHP for quiet sites, caches pages, compresses idle memory and protects the server when it runs out of memory. This page explains each mechanism and the capacity we measured.

The memory plan

ZoPanel splits RAM in two parts:

  • a system reserve for MariaDB, mail, nginx, the panel and the kernel;
  • a customer limit for everything customers run: PHP pools, apps, shells and Docker apps.

The customer limit is a hard limit. When a traffic burst wakes hundreds of sites at once, customer processes are slowed down and, at worst, ended inside that limit. PHP workers are ended first, and they restart at once. SSH, the panel, nginx and the databases keep their memory, so you can always log in and fix things.

The reserve is the larger of:

  • a fixed floor: a third of RAM, at least 1 GB but no more than 1 GB + 8% of RAM, plus about 0.75 MB per account;
  • what the system actually used at its peak over the last day, plus 256 MB and 3% of RAM.

The reserve is never more than half of RAM. ZoPanel re-plans it every 10 minutes. It can lower the reserve at once, but raises it by at most 512 MB per step, so customers' processes are not suddenly ended.

The current plan is shown in Settings → System → PHP performance.

The installer also sets MariaDB's buffer pool to 20% of RAM. On servers with less than 4 GB of RAM and no swap, it creates a swap file of 1 to 4 GB.

PHP that sleeps when idle

Each account runs its own PHP-FPM pool. A pool's master process uses about 25 MB even when nobody visits, so hundreds of quiet accounts would waste gigabytes. ZoPanel stops idle pools instead:

  1. PHP workers exit 10 seconds after their last request.
  2. A pool with no workers for the idle time (30 minutes by default) is stopped.
  3. The next visitor starts the pool again through its socket (systemd socket activation). This takes a few hundred milliseconds and no request is lost.

Change the idle time in Settings → System → PHP performance → Stop idle PHP after (details in PHP performance): 5, 15, 30 (recommended), 60 or 180 minutes, or Never (always running). The same card shows how many pools are running, how much memory PHP uses and the size of the compiled code cache.

When memory is short, idle pools are stopped at once, longest idle first, 20 per check, until memory recovers. A pool that is serving requests is never stopped.

For premium plans, turn on PHP always running in a package. Those sites' PHP never sleeps and starts at boot, at a cost of about 20–60 MB of RAM per site.

OPcache file cache

When a sleeping pool starts again, PHP loads its compiled scripts from disk instead of compiling them again. This makes the first visit almost as fast as a warm one. Each account has its own private cache folder under /var/cache/zopanel/opcache/. Scripts not loaded for 30 days are deleted.

Page cache

The page cache serves pages from nginx's cache, so PHP and MariaDB do not run for most visitors. It is the biggest single speed-up for WordPress.

Turn it on per website in Websites → (site) → PHP & config → Page cache. You can also use the Tuning advisor's Apply button to turn it on for every WordPress site that does not have it.

  • Logged-in users, admin pages, carts, checkout and POST requests always bypass the cache.
  • Pages are cached for 10 minutes. An expired page is served at once while one request refreshes it in the background. Pages not used for a day leave the cache.
  • WordPress sites get a small must-use plugin that purges their cached pages when content changes (posts, comments, menus, theme, plugins), so edits show at once.
  • Purge cache empties a site's cache by hand. Responses carry an X-Cache header (HIT, MISS, …).

For wp-admin, WooCommerce and logged-in users, the Redis object cache on the same tab keeps database query results in a private Redis instance for each account.

Memory compression

With Linux 6.8 or newer, ZoPanel compresses customers' idle memory in RAM with zswap. This covers PHP masters, OPcache and sleeping apps.

  • Customer pages are never written to disk. A page that does not fit compressed stays in RAM.
  • The compression ratio we measured is about 3.8–3.9 to 1. The Tuning page shows the current ratio.
  • zswap needs a swap device, but customer memory never goes to it. Without swap, compression stays off.
System Kernel What to do
Ubuntu 24.04, Debian 13 6.8 or newer Nothing
Ubuntu 22.04 5.15 The installer adds Ubuntu's HWE kernel by default
Debian 12 6.1 The installer asks; pass --kernel-backports to install the 6.12 backports kernel without asking

To upgrade the kernel on a server that is already installed, use the command the Tuning page suggests, then reboot:

# Ubuntu 22.04
apt install --install-recommends linux-generic-hwe-22.04 && reboot

# Debian 12
echo 'deb http://deb.debian.org/debian bookworm-backports main' > /etc/apt/sources.list.d/backports.list
apt update && apt install -t bookworm-backports linux-image-amd64 && reboot

On an ARM64 Debian server, install linux-image-arm64 instead. If the server has no swap, add it with the Tuning page's Apply button.

Load shedding

When the server is badly overloaded, PHP can queue up hundreds of requests whose visitors have already given up. Running them all keeps pools awake and the server short of memory long after the traffic stops.

ZoPanel sheds that load only in a real emergency. Customer memory must have been nearly at its limit, or customer processes stalled waiting for memory 70% of the time, for at least a minute. Then, when a pool's queue stays full for 30 seconds, the waiting requests are dropped, at most once a minute per pool.

During overload, a PHP or app website shows a "busy, please retry" page. It is sent with HTTP 503 and Retry-After: 30, so search engines treat it as temporary. If the site defines its own 502/503/504 error page, that page is used instead.

Measured capacity

These are results from our test servers, with WordPress sites plus 30 other apps on each server: 10 Node.js + MariaDB, 5 Flask + PostgreSQL, and 15 Joomla, phpBB or OpenCart sites.

Server WordPress, no page cache WordPress, page cache
Ubuntu 24.04, 4 GB RAM, 4 vCPU 100 sites + 30 apps 250 sites + 30 apps
Debian 12, 3 GB RAM, 4 vCPU (full profile) 50 sites + 30 apps 200 sites + 30 apps

After an 8-minute heavy overload, the servers were back to normal 5 minutes (Ubuntu) and 2 minutes (Debian) after the load stopped. Your results depend on the sites, plugins and traffic.

Tuning tips

  1. Turn on the page cache for every WordPress site that does not need to be dynamic for visitors. It is the change that adds the most capacity.
  2. Use a 6.8+ kernel so memory compression can work.
  3. Keep the default idle time (30 minutes). Shorten it on crowded servers; use PHP always running only for premium plans.
  4. Size packages realistically. The advisor warns when the PHP workers allowed in total do not fit in RAM. Lower PHP workers in packages or add RAM.
  5. Follow the Tuning page. Apply its suggestions for the MariaDB buffer, swap and nginx workers, and watch the disk forecast.
  6. Use the Redis object cache for WooCommerce and membership sites, where many pages bypass the page cache.
  7. Set a request rate limit on sites hit by bots (PHP & config → Request rate limit).

← Operations and monitoring App Store and S3 storage →