Scaling WooCommerce on DigitalOcean to Handle 50,000+ Concurrent Requests
Architectural Blueprint: WooCommerce at Scale on DigitalOcean
Achieving 50,000+ concurrent requests for a WooCommerce store isn’t a matter of tweaking a few settings; it requires a robust, multi-layered architecture. This document outlines a production-ready setup on DigitalOcean, focusing on high-availability, performance, and scalability. We’ll cover load balancing, database optimization, caching strategies, and application-level tuning.
Load Balancing with HAProxy for High Availability
A single web server will quickly become a bottleneck. We’ll deploy HAProxy as a TCP load balancer to distribute traffic across multiple WooCommerce application servers. This ensures no single server is overwhelmed and provides failover capabilities.
First, provision at least two HAProxy droplets. For this scale, consider 4-8 vCPU droplets with 8-16GB RAM each. Configure them for active-passive or active-active failover. For simplicity, we’ll illustrate an active-passive setup using Keepalived.
HAProxy Configuration
On each HAProxy server, install HAProxy and Keepalived.
Install HAProxy:
sudo apt update sudo apt install haproxy keepalived -y
Configure HAProxy (/etc/haproxy/haproxy.cfg):
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http_frontend
bind *:80
acl is_static url_static -i .jpg .jpeg .gif .png .css .js .ico .svg .woff .woff2
use_backend static_backend if is_static
default_backend app_backend
frontend https_frontend
bind *:443 ssl crt /etc/ssl/private/your_domain.pem # Ensure your SSL cert is here
acl is_static url_static -i .jpg .jpeg .gif .png .css .js .ico .svg .woff .woff2
use_backend static_backend if is_static
default_backend app_backend
backend app_backend
balance roundrobin
option httpchk GET /healthz # Simple health check endpoint
server app1 10.10.0.1:80 check # Replace with your app server IPs
server app2 10.10.0.2:80 check
server app3 10.10.0.3:80 check
server app4 10.10.0.4:80 check
backend static_backend
balance roundrobin
option httpchk GET /healthz
server static1 10.10.0.5:80 check # Dedicated static file server or CDN origin
server static2 10.10.0.6:80 check
Create a simple health check endpoint (public_html/healthz or similar) on your WooCommerce servers:
<?php
header('HTTP/1.1 200 OK');
echo 'OK';
exit;
?>
Keepalived Configuration for Failover
Configure Keepalived (/etc/keepalived/keepalived.conf) on both HAProxy nodes. One will be MASTER, the other BACKUP.
Node 1 (MASTER):
[global_defs]
router_id LVS_DEVEL
[vrrp_script chk_haproxy]
script "killall -0 haproxy"
interval 2
weight 2
[vrrp_instance VI_1]
state MASTER
interface eth0 # Your primary network interface
virtual_router_id 51
priority 150 # Higher priority for MASTER
advert_int 1
authentication
auth_type PASS
auth_pass 1234
virtual_ipaddress
192.168.1.100/24 # Your virtual IP address
Node 2 (BACKUP):
[global_defs]
router_id LVS_DEVEL
[vrrp_script chk_haproxy]
script "killall -0 haproxy"
interval 2
weight 2
[vrrp_instance VI_1]
state BACKUP
interface eth0 # Your primary network interface
virtual_router_id 51
priority 100 # Lower priority for BACKUP
advert_int 1
authentication
auth_type PASS
auth_pass 1234
virtual_ipaddress
192.168.1.100/24 # Your virtual IP address
Restart HAProxy and Keepalived on all nodes:
sudo systemctl restart haproxy sudo systemctl restart keepalived
Ensure your DigitalOcean firewall rules allow traffic on ports 80 and 443 to your HAProxy nodes, and that HAProxy nodes can communicate with your application servers on port 80.
WooCommerce Application Servers: Nginx, PHP-FPM, and OpCache
Each application server will run Nginx as the web server, serving static assets and proxying dynamic requests to PHP-FPM. PHP-FPM must be tuned for high concurrency, and OpCache must be aggressively configured.
For 50,000+ concurrent requests, you’ll need a cluster of application servers. Start with 4-8 droplets per cluster, each with 4-8 vCPUs and 8-16GB RAM. Scale horizontally by adding more droplets to the HAProxy backend.
Nginx Configuration
Install Nginx and PHP-FPM:
sudo apt update sudo apt install nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl php-zip -y
Configure Nginx server block (/etc/nginx/sites-available/woocommerce):
server {
listen 80;
server_name your_domain.com;
root /var/www/html/your_woocommerce_directory; # Your WooCommerce installation path
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version if needed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
log_not_found off;
}
location ~ /\.ht {
deny all;
}
# WooCommerce specific optimizations
location ~* ^/(wp-content/uploads|wp-includes)/.*.(css|js|jpg|jpeg|gif|png|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
log_not_found off;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;";
# 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 image/svg+xml;
}
Enable the site and test Nginx configuration:
sudo ln -s /etc/nginx/sites-available/woocommerce /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx
PHP-FPM Tuning
Edit the PHP-FPM pool configuration (e.g., /etc/php/8.1/fpm/pool.d/www.conf). For high concurrency, use the dynamic process manager and tune pm.max_children based on your server’s RAM. A common starting point is to allocate 20-30MB per PHP-FPM worker.
[www] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 150 ; Adjust based on RAM (e.g., 150 * 30MB = 4.5GB) pm.start_servers = 20 pm.min_spare_servers = 10 pm.max_spare_servers = 30 pm.process_idle_timeout = 10s pm.max_requests = 500 ; Restart workers after 500 requests to prevent memory leaks
Also, tune PHP’s core settings in /etc/php/8.1/fpm/php.ini:
memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 120 max_input_vars = 3000 realpath_cache_size = 4M realpath_cache_ttl = 600
Restart PHP-FPM:
sudo systemctl restart php8.1-fpm
OpCache Configuration
OpCache is critical for PHP performance. Ensure it’s enabled and aggressively configured in /etc/php/8.1/fpm/php.ini:
[opcache] opcache.enable=1 opcache.enable_cli=1 opcache.memory_consumption=256 ; Adjust based on your PHP memory usage opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 ; Increase for large sites opcache.revalidate_freq=60 ; Revalidate every 60 seconds opcache.validate_timestamps=1 ; Set to 0 in production if you have a robust deployment process opcache.save_comments=1 opcache.load_comments=1 opcache.enable_file_override=0 opcache.optimization_level=0xFFFFFFFF
Verify OpCache status using a tool like OPcache Control Panel or by creating a simple PHP file:
<?php phpinfo(); ?>
Restart PHP-FPM after changes.
Database Layer: PostgreSQL with Read Replicas and Connection Pooling
MySQL can struggle at this scale, especially with complex WooCommerce queries. PostgreSQL often offers better performance and scalability for demanding workloads. We’ll use a managed PostgreSQL instance on DigitalOcean with read replicas.
Provision a DigitalOcean Managed PostgreSQL cluster. Start with a sufficiently sized primary node (e.g., 8-16 vCPU, 32-64GB RAM) and at least two read replicas. For 50,000+ concurrent requests, consider a larger primary and more replicas.
Connection Pooling with PgBouncer
Direct connections from many application servers to the database can exhaust its resources. PgBouncer acts as a lightweight connection pooler, significantly reducing the load on the PostgreSQL server.
Deploy PgBouncer on a dedicated droplet (or co-located on app servers if resources permit). Ensure it can reach your PostgreSQL primary and replicas.
Install PgBouncer:
sudo apt update sudo apt install pgbouncer -y
Configure PgBouncer (/etc/pgbouncer/pgbouncer.ini):
[databases] # Format: db_name = host:port # Use a separate pool for read replicas db_read_replica1 = your_replica_1_host:5432 db_read_replica2 = your_replica_2_host:5432 [pgbouncer] logfile = /var/log/postgresql/pgbouncer/pgbouncer.log pidfile = /var/run/pgbouncer/pgbouncer.pid ; Connection pooling settings pool_mode = transaction ; 'session' or 'transaction'. 'transaction' is generally better for web apps. max_client_conn = 2000 ; Max concurrent clients to pgbouncer default_pool_size = 100 ; Max connections per database in the pool min_pool_size = 5 reserve_pool_size = 5 ; Authentication auth_type = md5 auth_user = pgbouncer_user auth_file = /etc/pgbouncer/userlist.txt ; Listen address listen_addr = 0.0.0.0:6432 ; Port for clients to connect to listen_port = 6432 ; Database connection strings (using the names defined in [databases]) ; For read replicas, you'll need to configure multiple entries or use a load-balancing approach ; For simplicity, we'll point to one replica here and manage others via application logic or a separate pgbouncer instance. ; A more advanced setup might use a load balancer in front of replicas and point pgbouncer to that. ; For this example, we'll use a single pool for reads. ; For writes, you'd typically connect directly to the primary or use a separate pgbouncer pool for writes. ; Example for read replicas (assuming a single pool for all read replicas) ; You might need to configure multiple pgbouncer instances or a more sophisticated routing. ; For simplicity, let's assume a single pgbouncer instance handling both reads and writes, ; and the application logic directs read queries to replicas. ; A better approach is to have separate pgbouncer pools for read and write. ; Let's configure for read/write separation: ; Primary DB connection ; This requires a separate pgbouncer config or careful application routing. ; For this example, we'll assume the application connects to pgbouncer on 6432 for writes, ; and a separate pgbouncer instance or direct connection for reads. ; A more robust setup: ; 1. Primary Pgbouncer instance for writes (connects to primary DB) ; 2. Secondary Pgbouncer instance for reads (connects to read replicas, potentially behind a load balancer) ; For simplicity in this example, we'll configure a single pgbouncer instance and rely on application logic to route. ; This is NOT ideal for massive scale but illustrates the concept. ; A production setup would use separate pools or instances. ; Let's assume the application connects to pgbouncer on 6432. ; We'll configure it to connect to the primary for writes and use a read replica for reads. ; This requires the application to know which connection string to use. ; A common pattern is to have two connection strings in the app config: one for writes, one for reads. ; For this example, let's configure pgbouncer to connect to the primary, and the app will handle read routing. ; This is a simplification. A true read/write split with pgbouncer is more complex. ; Let's assume the app connects to pgbouncer on 6432 for writes. ; For reads, the app will connect to a different pgbouncer instance or directly to replicas. ; Simplified config for writes: ; database = host=your_primary_db_host port=5432 dbname=your_db_name ; This requires the app to connect to pgbouncer on 6432 for writes. ; For read replicas, you'd typically have a separate pgbouncer instance or a load balancer. ; Let's configure a separate pool for reads. ; This requires the application to connect to a different port or pgbouncer instance for reads. ; Let's configure pgbouncer to manage connections to the primary DB. ; The application will connect to pgbouncer on port 6432 for writes. ; For reads, the application will connect to a separate pgbouncer instance or directly to replicas. ; Example for primary DB connection: database = your_db_name = host=your_primary_db_host port=5432 dbname=your_db_name ; Example for read replica connection (requires separate pgbouncer config or app logic) ; For this example, we'll assume the application connects to pgbouncer on 6432 for writes, ; and uses a separate mechanism for reads. ; A more practical approach for read/write splitting with pgbouncer: ; 1. Configure pgbouncer to connect to the primary DB. Application uses this for writes. ; 2. Configure a separate pgbouncer instance (or a different pool in the same instance) ; to connect to read replicas. Application uses this for reads. ; Let's configure the primary pool: [databases] your_db_name = host=your_primary_db_host port=5432 dbname=your_db_name ; And a pool for read replicas (assuming a single pool for all replicas) [databases] your_db_name_read = host=your_replica_1_host port=5432 dbname=your_db_name ; If you have multiple replicas, you might need to configure them individually or use a load balancer. ; For simplicity, let's assume a single pool for reads. ; This means the application needs to connect to pgbouncer on different ports or use different connection strings. ; For example, writes to 6432, reads to 6433 (if configured on a different port). ; Let's simplify and assume one pgbouncer instance, and the application logic handles routing. ; This is a compromise for illustration. ; Final simplified config for pgbouncer.ini: [databases] # Primary DB for writes primary_db = host=your_primary_db_host port=5432 dbname=your_db_name # Read Replicas (assuming a single pool for all read replicas) # In a real scenario, you might have multiple entries or a load balancer. read_replica_pool = host=your_replica_1_host port=5432 dbname=your_db_name [pgbouncer] logfile = /var/log/postgresql/pgbouncer/pgbouncer.log pidfile = /var/run/pgbouncer/pgbouncer.pid listen_addr = 0.0.0.0 listen_port = 6432 auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction max_client_conn = 2000 default_pool_size = 100 min_pool_size = 5 reserve_pool_size = 5 ; Uncomment for admin interface admin_users = pgbouncer_admin stats_users = pgbouncer_stats ; Uncomment for logging ; loglevel = 2 ; log_connections = 1 ; log_disconnections = 1 ; log_pooler_errors = 1
Create the userlist file (/etc/pgbouncer/userlist.txt). Ensure the user exists in your PostgreSQL database.
"pgbouncer_user" "md5$your_hashed_password" "pgbouncer_admin" "md5$your_hashed_password_for_admin"
Restart PgBouncer:
sudo systemctl restart pgbouncer
Update your WooCommerce database connection details (wp-config.php) to point to PgBouncer. For read/write splitting, you’ll need to modify your application code or use a plugin that supports it. A common approach is to define two database configurations:
define( 'DB_HOST', '127.0.0.1:6432' ); // Connect to Pgbouncer for writes define( 'DB_NAME', 'your_db_name' ); define( 'DB_USER', 'pgbouncer_user' ); define( 'DB_PASSWORD', 'your_password' ); define( 'DB_CHARSET', 'utf8mb4' ); define( 'DB_COLLATE', '' ); // For read replicas, you'd typically use a separate connection. // This requires application-level logic or a plugin. // Example: // define( 'DB_HOST_READ', '127.0.0.1:6433' ); // Assuming a second pgbouncer instance on port 6433 for reads // define( 'DB_NAME_READ', 'your_db_name' ); // define( 'DB_USER_READ', 'pgbouncer_user' ); // define( 'DB_PASSWORD_READ', 'your_password' ); // ... and then modify WordPress core or plugins to use these for SELECT queries. // This is a significant undertaking. A simpler approach is to use a plugin that handles read/write splitting.
Caching Strategies: Object Cache, Page Cache, and CDN
Aggressive caching is paramount. We’ll implement multiple layers of caching.
Redis for Object Caching
Use Redis to cache database query results, transient data, and WooCommerce objects. Deploy a managed Redis instance on DigitalOcean.
Install the Redis Object Cache plugin for WordPress. Ensure your application servers can connect to the Redis instance.
In wp-config.php:
define('WP_REDIS_CLIENT', 'phpredis');
define('WP_REDIS_HOST', 'your_redis_host');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PASSWORD', 'your_redis_password');
define('WP_REDIS_DATABASE', 0);
Nginx FastCGI Cache
Cache full HTML pages generated by WordPress. This bypasses PHP execution for most requests, dramatically improving performance.
Configure Nginx (add to your /etc/nginx/sites-available/woocommerce file):
# Define cache zone
fastcgi_cache_path /var/cache/nginx/woocommerce levels=1:2 keys_zone=woocommerce:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Add to your http, server, or location block
# Example within the server block:
server {
# ... other configurations ...
# Enable FastCGI cache
fastcgi_cache woocommerce;
fastcgi_cache_valid 200 30s; # Cache 200 responses for 30 seconds
fastcgi_cache_valid 301 1h;
fastcgi_cache_valid 302 1h;
fastcgi_cache_valid 404 1m;
fastcgi_cache_use_stale error timeout invalid_header updating http_500;
fastcgi_cache_min_uses 1;
fastcgi_cache_lock on; # Prevent cache stampede
# Add cache bypass rules for logged-in users, WooCommerce cart/checkout, etc.
# This is CRITICAL to avoid serving cached pages to users who shouldn't see them.
if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass|woocommerce_items_in_cart|woocommerce_cart_hash") {
set $nocache 1;
}
if ($request_method = POST) {
set $nocache 1;
}
if ($request_uri ~* "/(wp-admin/|wp-login.php|cart/|checkout/|my-account/)") {
set $nocache 1;
}
fastcgi_cache_bypass $nocache;
fastcgi_cache_unvalid $nocache;
# ... rest of your server block ...
}
# Ensure the cache directory exists and has correct permissions
# Run this on your app servers:
# sudo mkdir -p /var/cache/nginx/woocommerce
# sudo chown www-data:www-data /var/cache/nginx/woocommerce
# sudo chmod 755 /var/cache/nginx/woocommerce
You’ll likely need a WordPress plugin (e.g., WP Rocket, W3 Total Cache) to manage cache clearing when content is updated. Ensure these plugins are configured to purge the Nginx FastCGI cache.
Content Delivery Network (CDN)
Offload static assets (images, CSS, JS) to a CDN like Cloudflare, Amazon CloudFront, or BunnyCDN. This reduces load on your origin servers and improves global delivery speed.
Configure your WordPress site to serve assets from the CDN. Many caching plugins offer CDN integration.
Application-Level Optimizations
Beyond infrastructure, fine-tuning WooCommerce and WordPress itself is essential.
WooCommerce Settings
Disable features you don’t use. For example, if you don’t need product reviews, disable them. Reduce the number of products displayed per page in WooCommerce settings.
WordPress Core & Plugin Optimizations
Limit Plugin Usage: Only use essential, well-coded plugins. Each plugin adds overhead.
Optimize Images: Use image optimization plugins (e.g., Smush, ShortPixel) and ensure images are appropriately sized.
Disable Unused Features: Turn off WordPress heartbeat API if not needed, disable XML-RPC if not used for remote publishing.
Database Cleanup: Regularly clean up post revisions, transients, and spam comments using plugins or WP-CLI.
# Example WP-CLI commands wp post delete --post_type=revision --days=7 --force wp transient delete --all --force wp comment delete --status=spam --force
Monitoring and Alerting
Continuous monitoring is crucial. Set up alerts for key metrics:
- CPU, RAM, Disk I/O on all servers (HAProxy, App Servers, DB, Redis).
- Network traffic.
- HAProxy backend health checks.
- PHP-FPM process count.
- Database connection count and query latency.
- Redis memory usage and hit rate.
- Application error rates (e.g., 5xx errors).
Tools like Prometheus with Grafana, Datadog, or DigitalOcean’s built