Scaling WordPress on OVH to Handle 50,000+ Concurrent Requests
Architectural Foundation: OVH Dedicated Servers & Load Balancing
Achieving 50,000+ concurrent requests for a WordPress site on OVH necessitates a robust, multi-layered architecture. We’ll leverage OVH’s dedicated server offerings for raw performance and their advanced load balancing capabilities to distribute traffic efficiently. This isn’t about tweaking a single server; it’s about building a distributed system.
Dedicated Server Selection & Initial Configuration
For this scale, we’re looking at OVH’s “Infrastructure” dedicated servers. A minimum of two high-core-count servers (e.g., AMD EPYC-based) is recommended for the web tier, with additional servers for the database tier. The exact model depends on the specific workload (CPU-bound vs. I/O-bound), but prioritize ample RAM (128GB+) and fast NVMe storage.
Initial OS setup should be minimal. Ubuntu LTS or CentOS Stream are solid choices. Harden the OS immediately:
- Disable root SSH login and use key-based authentication for a dedicated admin user.
- Configure a firewall (e.g., UFW or firewalld) to allow only necessary ports (SSH, HTTP, HTTPS).
- Install and configure `fail2ban` to protect against brute-force attacks.
Web Server Tier: Nginx & PHP-FPM Optimization
Nginx is the de facto standard for high-performance WordPress serving due to its event-driven architecture. We’ll run multiple PHP-FPM pools, each tuned for concurrency.
Nginx Configuration Snippet (/etc/nginx/nginx.conf):
user www-data;
worker_processes auto; # Set to number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 8192; # Adjust based on system limits and expected load
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Important for security
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
# Include site configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
PHP-FPM Pool Configuration (e.g., /etc/php/8.2/fpm/pool.d/www.conf):
We’ll use the ondemand process manager for dynamic scaling, but with aggressive settings. For extreme loads, a static or dynamic pool with a high pm.max_children might be necessary, but requires careful tuning to avoid exhausting server resources.
[www] user = www-data group = www-data listen = /run/php/php8.2-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = ondemand pm.max_children = 100 ; Start with a high value, monitor and adjust pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Prevent memory leaks request_terminate_timeout = 60s request_slowlog_timeout = 30s catch_workers_output = yes ; rlimit_files = 10240 ; Increase if needed ; rlimit_nofile = 65536 ; Increase if needed ; For WordPress, ensure sufficient memory limit memory_limit = 512M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 120
Nginx Site Configuration (/etc/nginx/sites-available/your-wordpress-site):
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
# Redirect HTTP to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com www.your-domain.com;
root /var/www/your-wordpress-site/public_html;
index index.php index.html index.htm;
# SSL Certificate configuration (using Let's Encrypt example)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none';"; # Uncomment and configure carefully
# WordPress permalinks
location / {
try_files $uri $uri/ /index.php?$args;
}
# Serve static files directly
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
access_log off;
add_header Cache-Control "public";
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# FastCGI buffer tuning for large requests
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_read_timeout 300s; # Increase for potentially long-running scripts
}
# Deny access to wp-config.php
location ~ wp-config.php {
deny all;
}
# Caching for static assets (e.g., using Nginx cache or FastCGI cache)
# Example: FastCGI cache for PHP output (requires additional configuration)
# fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
# fastcgi_cache_key "$scheme$request_method$host$request_uri";
# add_header X-FastCGI-Cache $upstream_cache_status;
# location ~ \.php$ {
# ...
# fastcgi_cache WORDPRESS;
# fastcgi_cache_valid 200 60m; # Cache valid pages for 60 minutes
# fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
# }
}
Database Tier: High-Availability MySQL/MariaDB Cluster
A single database server will be a bottleneck. We need a highly available, performant database cluster. OVH’s Managed Databases or self-hosted Galera Cluster on dedicated servers are viable options. For this scale, self-hosting offers more control and potentially better performance tuning.
Galera Cluster Setup (Conceptual):
- Minimum of 3 nodes for quorum.
- Use Percona XtraDB Cluster or MariaDB Galera Cluster.
- Configure `wsrep_provider`, `wsrep_cluster_address`, `wsrep_cluster_name`, `wsrep_sst_method` (e.g., `xtrabackup-v2`).
- Tune `innodb_buffer_pool_size` aggressively (e.g., 70-80% of available RAM).
- Set `innodb_flush_log_at_trx_commit = 2` for better write performance, understanding the slight risk of data loss on a crash.
- Use `sync_binlog = 0` or `sync_binlog = 1` with caution, depending on RPO requirements.
- Optimize `max_connections` based on application needs and server resources.
- Consider dedicated read replicas if read load is significantly higher than write load.
MySQL/MariaDB Configuration Snippet (my.cnf):
[mysqld] # General user = mysql pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log # General Log disabled for production # general_log = 0 # general_log_file = /var/log/mysql/mysql.log # Galera Provider Configuration wsrep_on = ON wsrep_provider = /usr/lib/galera/libgalera_smm.so wsrep_cluster_name = "my_galera_cluster" wsrep_cluster_address = "gcomm://192.168.1.101,192.168.1.102,192.168.1.103" # IPs of your cluster nodes wsrep_node_address = "192.168.1.101" # Specific to each node wsrep_node_name = "node1" # Specific to each node wsrep_sst_method = xtrabackup-v2 wsrep_sst_auth = "sstuser:sstpassword" # InnoDB Configuration innodb_engine = InnoDB innodb_buffer_pool_size = 96G ; Adjust based on server RAM (e.g., 70-80%) innodb_log_file_size = 1G innodb_log_buffer_size = 128M innodb_flush_log_at_trx_commit= 2 ; Performance vs. durability trade-off innodb_flush_method = O_DIRECT innodb_io_capacity = 4000 ; Tune based on disk speed innodb_io_capacity_max = 8000 innodb_file_per_table = ON innodb_stats_on_metadata = OFF # Replication & Concurrency binlog_format = ROW default_storage_engine = InnoDB max_connections = 1000 ; Adjust based on load and server resources thread_cache_size = 50 table_open_cache = 4000 table_definition_cache = 1000 query_cache_type = 0 ; Query cache is deprecated and often problematic query_cache_size = 0 slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 # Galera specific tuning innodb_autoinc_lock_mode = 2 ; For better concurrency with Galera ```
Caching Strategy: Multi-Layered Approach
Caching is paramount. We’ll implement a multi-layered strategy:
- Object Cache (Redis/Memcached): Essential for reducing database load. Use a robust WordPress plugin like W3 Total Cache or WP Rocket configured with Redis. Deploy Redis on separate, powerful servers.
- Page Cache (Nginx FastCGI Cache or Varnish): Nginx’s built-in FastCGI cache can serve static HTML directly, bypassing PHP entirely for many requests. Varnish offers more advanced features but adds complexity.
- CDN (Content Delivery Network): Offload static assets (images, CSS, JS) to a CDN like Cloudflare, Akamai, or OVH’s own CDN. This dramatically reduces load on your origin servers.
- Browser Cache: Leverage HTTP headers (`Expires`, `Cache-Control`) configured in Nginx to instruct browsers to cache static assets.
Redis Configuration Snippet (/etc/redis/redis.conf):
# General daemonize yes pidfile /var/run/redis/redis-server.pid logfile /var/log/redis/redis-server.log dir /var/lib/redis # Network bind 127.0.0.1 -::1 # Bind to localhost if only used locally, or specific IPs if accessed by web servers protected-mode yes port 6379 # Memory Management maxmemory 16gb # Adjust based on available RAM maxmemory-policy allkeys-lru # Evict least recently used keys when maxmemory is reached # Persistence (disable for pure cache, enable if data persistence is needed) save "" appendonly no # Replication (if setting up Redis Sentinel or Cluster) # replica-serve-stale-data yes # replica-read-only yes # Security # requirepass your_strong_redis_password # Uncomment and set a strong password # rename-command CONFIG "" # Disable dangerous commands # rename-command FLUSHALL ""
OVH Load Balancer Configuration
OVH’s load balancer is critical for distributing traffic across your Nginx web servers and handling SSL termination. We’ll configure it for HTTP/2 and sticky sessions if necessary (though ideally, WordPress is stateless).
Key Load Balancer Settings:
- Frontend Configuration: Listen on ports 80 and 443. Terminate SSL here using your certificates. Enable HTTP/2.
- Backend Pool: Add your Nginx web servers.
- Load Balancing Algorithm: Round Robin is a good default. Least Connections can be effective if server loads vary significantly.
- Health Checks: Configure robust health checks (e.g., checking for a specific HTTP status code on a health check URL like
/healthz) to automatically remove unhealthy servers from rotation. - Session Persistence (Sticky Sessions): Generally avoid for WordPress unless absolutely necessary due to plugin dependencies. If required, configure cookie-based persistence.
- WAF (Web Application Firewall): Enable OVH’s WAF for an additional layer of security against common web attacks.
WordPress Optimization & Security Plugins
Even with a strong infrastructure, WordPress itself needs tuning:
- Caching Plugin: W3 Total Cache, WP Rocket, or LiteSpeed Cache (if using LiteSpeed web server). Configure object caching (Redis), page caching, and asset optimization.
- Image Optimization: Use plugins like Smush Pro or ShortPixel to compress images on upload.
- Database Optimization: Regularly clean up post revisions, transients, and optimize database tables using plugins like WP-Optimize.
- Security Plugin: Wordfence or Sucuri Security for malware scanning, firewall rules, and brute-force protection.
- Disable Unused Features: Turn off pingbacks, trackbacks, and XML-RPC if not needed.
- Optimize Theme & Plugins: Choose well-coded themes and plugins. Audit and remove any unnecessary or poorly performing ones.
Monitoring & Performance Tuning Workflow
Continuous monitoring is non-negotiable. Set up comprehensive monitoring for:
- Server Resources: CPU, RAM, Disk I/O, Network traffic (using tools like `htop`, `vmstat`, `iostat`, Prometheus/Grafana).
- Nginx: Active connections, request rates, error rates (e.g., using `stub_status` module).
- PHP-FPM: Process manager status, request latency, slow requests.
- Database: Query performance, connection counts, replication lag, buffer pool hit rate.
- Application Performance Monitoring (APM): Tools like New Relic, Datadog, or open-source alternatives like Jaeger/Zipkin integrated with WordPress.
Tuning Workflow:
- Establish Baselines: Understand normal traffic patterns and resource utilization.
- Load Testing: Use tools like `k6`, `JMeter`, or `ApacheBench` (`ab`) to simulate traffic and identify bottlenecks *before* they occur in production.
- Analyze Slow Queries: Regularly review the MySQL slow query log.
- Profile PHP Code: Use tools like Xdebug with a profiler (e.g., KCacheGrind) to identify slow functions or plugin bottlenecks.
- Iterative Tuning: Adjust Nginx worker connections, PHP-FPM pool sizes, database buffer pool, and caching durations based on monitoring data and load test results.
- Review Logs: Regularly check Nginx, PHP-FPM, and system logs for errors or warnings.
Conclusion
Scaling WordPress to handle 50,000+ concurrent requests on OVH is an exercise in distributed systems engineering. It requires a high-performance hardware foundation, meticulous configuration of Nginx, PHP-FPM, and a robust database cluster, complemented by aggressive multi-layered caching and continuous monitoring. This architecture provides the necessary horsepower and resilience to serve a demanding audience.