Scaling Laravel on OVH to Handle 50,000+ Concurrent Requests
Architectural Foundation: Beyond Single Instances
Achieving 50,000+ concurrent requests for a Laravel application on OVH necessitates a fundamental shift from a monolithic, single-server deployment to a distributed, horizontally scalable architecture. This involves decoupling core components, leveraging managed services where appropriate, and implementing robust load balancing and caching strategies. We’ll focus on a typical OVH Public Cloud setup, assuming a baseline of compute instances, managed databases, and potentially object storage.
Load Balancing Strategy: HAProxy for High Availability
A critical first step is implementing a high-availability load balancer. While OVH offers its own load balancing services, deploying and managing HAProxy on a dedicated instance provides granular control and deep insights. We’ll configure HAProxy for TCP and HTTP load balancing, health checks, and sticky sessions if absolutely necessary (though stateless is preferred).
HAProxy Configuration for Laravel
This configuration assumes you have at least two Laravel application servers (e.g., `app_server_1` and `app_server_2`) running your PHP-FPM instances on port 9000, and your web server (Nginx) listening on port 80.
global
log 127.0.0.1 local0
log 127.0.0.1 local1 notice
maxconn 4096
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 host_static hdr(host) -i static.yourdomain.com
acl host_api hdr(host) -i api.yourdomain.com
acl host_app hdr(host) -i app.yourdomain.com
# Route static assets to a dedicated Nginx instance or CDN
use_backend static_servers if host_static
# Route API requests to a dedicated backend pool
use_backend api_servers if host_api
# Default to the main application servers
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /healthz HTTP/1.1\r\nHost:\ app.yourdomain.com
server app_server_1 192.168.1.10:80 check
server app_server_2 192.168.1.11:80 check
# Add more app servers as needed
backend api_servers
balance roundrobin
option httpchk GET /healthz HTTP/1.1\r\nHost:\ api.yourdomain.com
server api_server_1 192.168.1.20:80 check
server api_server_2 192.168.1.21:80 check
# Add more API servers if they are separate
backend static_servers
balance roundrobin
server static_server_1 192.168.1.30:80 check
# Or point to a CDN endpoint
listen stats
bind *:8404
mode http
stats enable
stats uri /haproxy?stats
stats realm Haproxy\ Statistics
stats auth admin:YourSecurePassword
Key Considerations:
- Health Checks: The
option httpchkdirective is crucial. Ensure your Laravel application has a dedicated health check endpoint (e.g.,/healthz) that returns a 200 OK status code. This endpoint should be lightweight and not perform expensive database queries. - Backend Servers: Replace the placeholder IP addresses with the actual private IPs of your Nginx/Laravel application servers within your OVH VPC.
- Statelessness: Design your Laravel application to be stateless. Avoid storing session data on the application servers themselves. Use a distributed cache like Redis or Memcached for sessions.
- Dedicated API/Static Backends: For optimal performance, consider separating API endpoints and static asset serving into distinct backend pools, potentially even on different server clusters or using a CDN.
- HAProxy Stats: The
listen statssection provides a web interface for monitoring HAProxy. Secure it with strong authentication.
Application Server Optimization: Nginx & PHP-FPM Tuning
Each application server needs to be meticulously tuned. This involves optimizing Nginx for high concurrency and PHP-FPM for efficient request processing.
Nginx Configuration (`nginx.conf` or site-specific conf)
user www-data;
worker_processes auto; # Set to the number of CPU cores
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096; # 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; # Hide Nginx version for security
# 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;
# Buffers and Timeouts
client_body_buffer_size 10M;
client_max_body_size 10M;
client_header_buffer_size 1k;
large_client_header_buffers 4 32k;
output_buffers 1 32k;
post_action @fallback; # For handling upstream timeouts gracefully
# FastCGI (PHP-FPM) configuration
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300; # Increase timeout for long-running scripts
fastcgi_connect_timeout 60;
fastcgi_send_timeout 300;
}
# Deny access to hidden files
location ~ /\.ht {
deny all;
}
# Serve static files directly
location ~* \.(css|js|jpg|jpeg|gif|png|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# Include other configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
# Fallback for upstream timeouts (optional but recommended)
location @fallback {
return 504 "Gateway Timeout";
}
PHP-FPM Configuration (`php-fpm.conf` and pool configuration)
Edit the PHP-FPM pool configuration file (e.g., `/etc/php/8.1/fpm/pool.d/www.conf`).
; Basic settings pid = /run/php/php8.1-fpm.pid error_log = /var/log/php8.1-fpm.log log_level = notice ; Process management ; Use 'dynamic' for typical scenarios, 'static' for predictable high load pm = dynamic pm.max_children = 250 ; Adjust based on available RAM and CPU pm.start_servers = 50 ; Initial number of children pm.min_spare_servers = 25 ; Minimum idle children pm.max_spare_servers = 100 ; Maximum idle children pm.max_requests = 5000 ; Restart children after this many requests ; Performance tuning request_terminate_timeout = 120 ; Max execution time for a script (seconds) ; Use 'ondemand' if you have very spiky traffic and want to save memory ; pm.process_idle_timeout = 10s ; For 'ondemand' ; Listen on a Unix socket for Nginx listen = /var/run/php/php8.1-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Security settings ; security.limit_extensions = .php .phtml ; Uncomment and adjust if needed
Tuning Notes:
- `worker_processes` (Nginx): Set this to the number of CPU cores on your Nginx server.
- `worker_connections` (Nginx): This defines the maximum number of simultaneous connections a worker can handle. The theoretical limit is `worker_processes * worker_connections`. Ensure your system’s file descriptor limits (`ulimit -n`) are high enough.
- `pm.max_children` (PHP-FPM): This is the most critical setting. It dictates how many PHP processes can run concurrently. A common formula is `(Total RAM – RAM for OS/Nginx) / Average RAM per PHP process`. Monitor memory usage closely.
- `fastcgi_read_timeout` (Nginx): Increase this if your Laravel application has long-running tasks.
- `request_terminate_timeout` (PHP-FPM): Corresponds to PHP’s `max_execution_time`.
- Socket vs. TCP: Using Unix sockets (`/var/run/php/phpX.Y-fpm.sock`) is generally faster than TCP/IP for communication between Nginx and PHP-FPM on the same server.
Database Scaling: OVH Managed PostgreSQL/MySQL
Your database is often the bottleneck. Relying on OVH’s managed database services (e.g., Managed PostgreSQL or Managed MySQL) is highly recommended for scalability and reliability. These services offer features like read replicas, automated backups, and failover.
Read Replicas for Offloading Reads
Configure at least one read replica for your primary database. Your Laravel application can then be configured to use the replica for read-heavy operations.
// config/database.php
'connections' => [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'), // Primary DB host
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
// ... other primary connection settings
],
'mysql_read' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL_READ'),
'host' => env('DB_HOST_READ', 'db-read-replica.ovh.local'), // Read replica host
'port' => env('DB_PORT_READ', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'options' => [
PDO::ATTR_TIMEOUT => 5, // Shorter timeout for replicas
],
],
],
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1), // Use a separate DB for cache
],
],
In your Laravel application code, you can then explicitly use the read connection:
use Illuminate\Support\Facades\DB;
// Using the default (write) connection
$users = DB::connection('mysql')->select('SELECT * FROM users');
// Using the read replica connection
$products = DB::connection('mysql_read')->select('SELECT * FROM products WHERE is_active = 1');
// Eloquent models can also be configured to use specific connections
// (Requires defining connection properties on the model or using traits)
Database Indexing and Query Optimization
Even with replicas, poorly optimized queries will cripple performance. Regularly analyze your slow query logs and ensure appropriate indexes are in place. Use tools like Laravel Debugbar in development and `EXPLAIN` in production to identify bottlenecks.
-- Example: Analyzing a slow query EXPLAIN SELECT p.*, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id WHERE p.price > 100 ORDER BY p.created_at DESC; -- If 'category_id' or 'price' are not indexed, add them: CREATE INDEX idx_products_category_id ON products (category_id); CREATE INDEX idx_products_price ON products (price);
Caching Strategies: Redis for Everything
Aggressive caching is non-negotiable. Redis is an excellent choice for various caching layers in a Laravel application.
Leveraging Laravel’s Cache Facade
Ensure your .env file is configured to use Redis for caching:
CACHE_DRIVER=redis REDIS_HOST=your-redis-host.ovh.internal REDIS_PASSWORD=null REDIS_PORT=6379 REDIS_DB=1
Implement caching for:
- Configuration: Laravel automatically caches config files. Run `php artisan config:cache`.
- Routes: Cache your routes for faster lookup. Run `php artisan route:cache`.
- Views: Compile and cache Blade views. Run `php artisan view:cache`.
- Database Query Results: Cache expensive or frequently accessed query results.
- API Responses: Cache responses from external APIs or computationally intensive internal endpoints.
- Sessions: As mentioned, use Redis for session storage to maintain statelessness.
use Illuminate\Support\Facades\Cache;
use App\Models\Product;
// Example: Caching a collection of products
$products = Cache::remember('all_active_products', now()->addMinutes(60), function () {
return Product::where('is_active', true)->get();
});
// Example: Caching a single model instance
$user = Cache::remember('user_' . $userId, now()->addHours(1), function () use ($userId) {
return User::findOrFail($userId);
});
// Example: Caching an API response
$apiResponse = Cache::remember('external_api_data_' . $cacheKey, now()->addMinutes(15), function () use ($apiEndpoint) {
// Make the API call here
$client = new \GuzzleHttp\Client();
$response = $client->get($apiEndpoint);
return json_decode($response->getBody(), true);
});
Queueing for Background Jobs
Any time-consuming task that doesn’t need to be part of the immediate HTTP request-response cycle should be offloaded to a background queue. This includes sending emails, processing images, generating reports, etc.
Redis as a Queue Driver
Configure your .env file to use Redis for queues:
QUEUE_CONNECTION=redis
Ensure you have Redis running and accessible. You can use OVH’s Managed Redis service or deploy your own.
Running Queue Workers
On your application servers (or dedicated worker servers), you need to run the queue worker processes. Use a process manager like `supervisor` to keep them running reliably.
# Start a queue worker php artisan queue:work --queue=default,high_priority --tries=3 --timeout=300 # Start multiple workers for different queues or to increase concurrency # Example supervisor configuration for a single worker: # /etc/supervisor/conf.d/laravel-queue.conf [program:laravel-queue] process_name=%(program_name)s_%(process_num)02d command=php /var/www/your-app/artisan queue:work redis --queue=default,high_priority --sleep=3 --tries=3 --timeout=300 autostart=true autorestart=true user=www-data numprocs=4 ; Number of concurrent workers for this queue redirect_stderr=true stdout_logfile=/var/log/supervisor/laravel-queue.log stderr_logfile=/var/log/supervisor/laravel-queue.err.log
Key Queue Settings:
- `–tries`: Number of times to attempt a failed job before giving up.
- `–timeout`: Maximum number of seconds a job can run.
- `–sleep`: Seconds to sleep if the queue is empty.
- `–memory`: Maximum amount of memory a worker may consume.
- `–queue`: Specify which queues the worker should process.
Monitoring and Alerting
A high-traffic system requires constant vigilance. Implement comprehensive monitoring for all components.
- Server Metrics: CPU, RAM, Disk I/O, Network Traffic (using tools like Prometheus/Grafana, Datadog, or OVH’s monitoring).
- Application Performance Monitoring (APM): Tools like New Relic, Datadog APM, or Sentry can provide deep insights into request tracing, database query times, and errors within your Laravel application.
- HAProxy Stats: Regularly check the HAProxy stats page for backend server health, request rates, and error counts.
- Queue Monitoring: Monitor the number of pending jobs in your Redis queue. Use tools like Laravel Horizon for a robust dashboard.
- Log Aggregation: Centralize logs from all servers (Nginx, PHP-FPM, Laravel logs) using a tool like the ELK stack (Elasticsearch, Logstash, Kibana) or Graylog.
Deployment and CI/CD
Automate your deployments to ensure consistency and reduce manual errors. A CI/CD pipeline is essential for managing multiple application servers.
- Version Control: Git is mandatory.
- CI/CD Platform: GitLab CI, GitHub Actions, Jenkins, CircleCI.
- Deployment Strategy: Blue-Green deployments or Rolling Updates to minimize downtime.
- Configuration Management: Tools like Ansible or Chef can automate server setup and configuration.
- Zero-Downtime Deployments: Ensure your deployment process involves updating application servers one by one, with the load balancer taking them out of rotation during the update.
Conclusion: Iterative Scaling
Scaling to 50,000+ concurrent requests is not a one-time task but an ongoing process. Start with these foundational elements: robust load balancing, optimized application servers, scalable database solutions, and aggressive caching. Continuously monitor performance, identify bottlenecks, and iterate on your architecture. OVH’s Public Cloud provides the building blocks, but a well-architected Laravel application and meticulous tuning are paramount.