Why Shared Hosting Density Still Matters#
The economics of shared hosting are unforgiving. Margins are razor-thin, and every extra site you can reliably stack on a single box directly improves your bottom line. While container-based isolation gains traction, traditional multi-tenant hosting remains the workhorse for agencies, indie hosts, and resellers.
A modern 64-core EPYC with 256 GB of RAM and NVMe storage can comfortably host thousands of sites—but only if Nginx and PHP-FPM are tuned for the workload. Default configurations are built for a handful of high-traffic applications, not ten thousand low-traffic WordPress installs.
This walkthrough assumes you're running a modern Linux stack (Ubuntu 22.04+ or AlmaLinux 9) with Nginx, PHP-FPM 8.2 or 8.3, and individual system users per account (via a control panel like cPanel, a custom provisioner, or a platform like Salieno Core). We'll cover process model selection, per-pool tuning, memory budgeting, and the kernel-level knobs that keep everything stable under bursty, multi-tenant load.
Step 1: Pick the Right PHP-FPM Process Manager#
PHP-FPM offers three process managers: static, dynamic, and ondemand. For shared hosting with thousands of idle sites, ondemand is almost always the correct choice.
Edit each pool config (typically in /etc/php/8.3/fpm/pool.d/):
[example_com]
user = example
group = example
listen = /run/php/example_com.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 30s
pm.max_requests = 500Why ondemand? It spawns zero PHP workers until a request arrives. A site that gets 50 hits/day consumes zero RAM between requests. The pm.max_children cap prevents a single noisy site from hogging all workers; five is a reasonable ceiling for a typical small site. pm.max_requests = 500 recycles workers periodically to guard against memory leaks—a common issue with poorly written WordPress plugins.
What about dynamic? Dynamic pre-spawns a reserve of idle workers per pool. With 10,000 pools, even pm.start_servers = 1 means 10,000 idle PHP processes consuming ~30 MB each. That's 300 GB of RAM gone before a single visitor arrives. Use dynamic only for your highest-traffic pools.
Step 2: Budget Your Memory#
Before tuning anything, do the math. For each PHP-FPM worker, expect 20–40 MB of RSS depending on loaded extensions (OPcache, ionCube, etc.). Let's use 35 MB as a conservative average.
On a 256 GB server, reserve:
- 8 GB for the OS, Nginx, and system services
- 16 GB for OPcache (shared across all pools)
- 8 GB for MySQL/MariaDB buffer pool
- 4 GB headroom for spikes
That leaves ~220 GB for PHP workers. At 35 MB each, you can safely run ~6,200 concurrent workers across all pools. With ondemand and an average of 0.3 active workers per site, you can host roughly 20,000 sites before hitting the ceiling. Adjust the numbers for your actual RSS.
Verify per-pool memory with:
ps -eo pid,user,rss,comm --sort=-rss | grep php-fpm | head -40Step 3: Tune OPcache for Multi-Tenancy#
A single shared OPcache instance works well for density, but the defaults are too conservative. Edit /etc/php/8.3/fpm/conf.d/10-opcache.ini:
opcache.enable = 1
opcache.memory_consumption = 16384
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 50000
opcache.revalidate_freq = 300
opcache.validate_timestamps = 1
opcache.save_comments = 1memory_consumption = 16384 allocates 16 GB—enough for tens of thousands of PHP files. revalidate_freq = 300 checks for file changes every 5 minutes instead of every request, dramatically reducing stat() syscalls. Keep validate_timestamps = 1 so deployments and plugin updates take effect without restarting FPM.
Step 4: Configure Nginx Worker Processes and Connections#
In /etc/nginx/nginx.conf:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
keepalive_timeout 30;
keepalive_requests 200;
reset_timedout_connection on;
client_body_timeout 10;
send_timeout 10;
}worker_processes auto sets one worker per CPU core. worker_connections 4096 per worker with 64 cores gives you 256,000 simultaneous connections—more than enough. The aggressive keepalive and timeout settings close idle connections quickly, preventing socket exhaustion under bursty traffic from thousands of low-traffic sites.
Step 5: Set Kernel Limits for Mass Hosting#
With thousands of sockets and open files, the default kernel limits will bite you. Add to /etc/sysctl.conf:
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 16384
fs.file-max = 2097152Apply with sysctl -p. Then raise the per-process file descriptor limit in /etc/security/limits.conf:
www-data soft nofile 65535
www-data hard nofile 65535Step 6: Monitor and Iterate#
Tuning isn't a one-time event. Install php-fpm_exporter and nginx-prometheus-exporter to feed Grafana dashboards. Key metrics to watch:
- PHP-FPM active/idle processes per pool – confirms your
max_childrencaps are working - OPcache hit rate – should be >95%; if lower, increase
memory_consumption - Nginx 502/503 errors – signals worker exhaustion; raise
max_childrenfor affected pools or add server capacity - OOM killer events in
dmesg– means your memory budget is wrong
A lightweight monitoring stack catches problems before customers notice. Most reseller platforms, including Salieno Core, expose per-account resource stats that complement system-level metrics.
The Bottom Line#
A well-tuned Nginx + PHP-FPM stack on modern hardware can reliably host 10,000+ low-traffic sites. The keys are ondemand process management, aggressive memory budgeting, generous OPcache, and kernel-level tuning for high file-descriptor counts. Get these right, and your cost-per-site drops dramatically—giving you the margin headroom to compete in a market where $5/month plans are the norm.
0 comments
Loading comments…