Scaling Applications on CloudPloy
CloudPloy runs your applications on AWS EC2 and Lightsail instances. When your application grows beyond your current server's capacity, you have several options: optimize the application to handle more load on the same hardware, scale vertically by upgrading to a larger server, or scale horizontally by adding more servers. This guide covers each approach and when to use it.
Before Scaling: Understand the Bottleneck
Adding server capacity doesn't help if the bottleneck is inefficient database queries, missing caches, or unoptimized code. Before scaling, identify where the constraint is:
| Symptom | Likely Bottleneck | First Fix |
|---|---|---|
| High CPU, slow PHP responses | PHP-FPM worker pool exhausted | Increase pm.max_children or add Redis object cache |
| Slow database queries, high DB CPU | Missing indexes or N+1 queries | Add indexes, enable query caching, add read replica |
| High memory, frequent OOM | Memory leaks or over-provisioned workers | Reduce pm.max_children, set max_requests to restart workers |
| Slow static assets | No CDN or caching headers | Enable Cloudflare caching, set long Cache-Control headers |
| Queue depth growing | Insufficient queue workers | Add more worker replicas in App > Workers |
For a running application, check the server's resource usage in Server > Monitoring. If CPU is consistently above 80% or memory is consistently above 85%, you need either optimization or vertical scaling. If CPU spikes during traffic peaks but is low otherwise, horizontal scaling helps.
PHP-FPM Tuning for More Concurrent Requests
PHP-FPM's worker pool is the first thing to tune for web applications. Each incoming HTTP request uses one PHP-FPM worker. If all workers are busy, new requests queue. If the queue fills, users get 502 errors.
Calculate the maximum safe number of workers for your server:
# Check available RAM for PHP-FPM
# Total RAM minus OS and other services (MySQL, Redis, Nginx)
# Each PHP-FPM worker uses 30-100MB depending on your app
# For a 4GB server running MySQL and Redis:
# Available for PHP: ~2GB
# Average worker memory: 50MB
# Max workers: 2000MB / 50MB = ~40 workers
# Check actual worker memory usage:
ps aux | grep php-fpm | awk '{print $6}' | sort -n | tail -20 Configure PHP-FPM in your Dockerfile or server configuration:
; php-fpm-pool.conf
[www]
pm = dynamic
pm.max_children = 40 ; Total worker cap
pm.start_servers = 10 ; Workers at startup
pm.min_spare_servers = 5 ; Minimum idle workers
pm.max_spare_servers = 20 ; Maximum idle workers
pm.max_requests = 500 ; Restart worker after 500 requests (prevents memory leaks)
pm.process_idle_timeout = 10s Use pm = dynamic (not static) - dynamic mode scales workers between min and max based on load, which uses less RAM during quiet periods and full capacity during peaks.
Vertical Scaling: Upgrading Your Server
Vertical scaling means moving your CloudPloy server to a larger AWS instance with more CPU cores and RAM. This is the simplest scaling option and works for most applications that have hit their server's capacity.
To resize a server, go to Server > Settings > Resize in the CloudPloy dashboard. CloudPloy stops the server, changes the instance type, and restarts it. Downtime is typically 2-5 minutes. Your applications, databases, and persistent storage are all preserved.
Common upgrade paths on AWS:
| Instance Type | vCPU | RAM | Good For |
|---|---|---|---|
| t3.small | 2 | 2GB | Small sites, staging |
| t3.medium | 2 | 4GB | Medium sites, low traffic |
| t3.large | 2 | 8GB | Medium-high traffic sites |
| t3.xlarge | 4 | 16GB | High traffic, multiple apps |
| c5.xlarge | 4 | 8GB | CPU-intensive applications |
| c5.2xlarge | 8 | 16GB | Very high traffic |
Note that t3 instances use CPU credits for burst workloads. For consistent high-CPU applications, c5 instances provide dedicated compute without credit throttling.
Horizontal Scaling: Adding More Servers
When a single server's vertical limit is not enough, or when you need to eliminate single points of failure, add more CloudPloy servers and route traffic across them. CloudPloy does not manage load balancing between servers automatically - you configure this at the DNS level using Cloudflare or AWS Route 53.
Setting Up Load Balancing with Cloudflare
- Provision additional CloudPloy servers in the same or different AWS regions
- Deploy your application to each server - they should be identical
- In Cloudflare, go to Traffic > Load Balancing and create a load balancer
- Add each server's IP as an origin in an origin pool
- Configure health checks so Cloudflare removes unhealthy servers automatically
- Set DNS to use the Cloudflare load balancer hostname
Cloudflare Load Balancing requires a paid Cloudflare plan - the add-on starts at $5/month for 2 origins. For simpler round-robin DNS without health checks, you can add multiple A records to your domain, though this doesn't remove failed servers from rotation automatically.
Session Handling Across Multiple Servers
When users can be routed to any server, file-based PHP sessions break - session data on server A is not available on server B. Before adding servers, move sessions to a centralized Redis instance:
# Laravel
SESSION_DRIVER=redis
REDIS_HOST=your-redis-host
# WordPress (requires Redis Object Cache plugin)
define('WP_REDIS_HOST', 'your-redis-host');
# PHP (generic)
session.save_handler = redis
session.save_path = "tcp://your-redis-host:6379" All servers share the same Redis instance for sessions, so any server can handle any user's request.
Shared File Storage
User-uploaded files stored on one server's disk are not available on other servers. Move file storage to S3 before adding servers:
# Laravel - S3 filesystem
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-uploads-bucket WordPress requires a plugin like WP Offload Media to serve uploads from S3 instead of the local filesystem.
Queue Worker Scaling
Queue workers scale independently from web servers. If your job queue is growing because workers can't keep up, add more worker replicas without touching the web application:
- Go to App > Workers in the CloudPloy dashboard
- Increase the replica count for your queue worker
- Each additional replica processes jobs concurrently
Monitor queue depth over time to understand normal patterns. A growing queue during peak hours that clears overnight is expected. A queue that never clears means you need more workers or faster job processing.
For Laravel, use Horizon to get real-time queue metrics:
composer require laravel/horizon
php artisan horizon:install
# In config/horizon.php - configure worker pools per queue
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
], Caching to Reduce Server Load
Caching is often the most effective scaling strategy - it eliminates repeat computations rather than requiring more servers to repeat them. The main caching layers for CloudPloy applications:
Cloudflare Page Caching
For public, non-authenticated pages, Cloudflare can cache full HTML responses at its edge network. Cached pages are served directly from Cloudflare without reaching your server. This is the highest-leverage caching for content sites and marketing pages.
Configure Cache-Control headers in your application to tell Cloudflare what to cache:
# Nginx configuration for WordPress - cache public pages for 1 hour
location ~ \.php$ {
# ... PHP-FPM config ...
add_header Cache-Control "public, max-age=3600" always;
} Cloudflare respects Cache-Control headers. Pages with cookies (logged-in users) are not cached by default.
Redis Object Caching
Store database query results, computed values, and API responses in Redis to avoid repeating expensive operations:
// Laravel - cache a database query for 10 minutes
$products = Cache::remember('products.featured', 600, function () {
return Product::featured()->with('images')->get();
});
// Cache user profile for 1 hour (invalidated on profile update)
$profile = Cache::remember("user.${userId}.profile", 3600, function () use ($userId) {
return User::with('preferences')->find($userId);
}); MySQL Query Cache and Indexes
Slow database queries are a common scalability bottleneck. Enable the MySQL slow query log to identify them:
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5 ; Log queries taking over 500ms
log_queries_not_using_indexes = ON Run EXPLAIN on slow queries and add indexes for columns used in WHERE, ORDER BY, and JOIN conditions. Adding the right index can turn a 5-second table scan into a sub-millisecond lookup.
Handling Traffic Spikes
For predictable traffic spikes (Black Friday, product launches, marketing campaigns), prepare in advance rather than relying on automatic response:
- Pre-warm caches: Run your application's cache warming before the event so the first real requests hit cached data
- Scale up ahead of time: Upgrade to a larger server or add servers before the event - adding capacity during a spike is slower than having it ready
- Pre-generate static content: For WordPress, pre-generate page caches for high-traffic pages
- Enable rate limiting: Configure Nginx or your application to rate-limit requests per IP to prevent one bad actor from consuming all capacity
- Test under load: Run a load test (k6, Artillery, or Locust) against your staging environment before the event to find the actual capacity limit
For unpredictable spikes, the most reliable protection is aggressive caching and a CDN in front of your application. A page cached by Cloudflare handles unlimited traffic without touching your server.
Scaling WordPress Specifically
WordPress generates significant database write traffic by default. Before adding servers, apply these WordPress-specific optimizations:
- Redis Object Cache plugin: Caches database queries in Redis. Reduces MySQL load by 50-80% for content-heavy sites
- Disable post revisions: Add
define('WP_POST_REVISIONS', 3);to limit revision storage - Disable pingbacks and trackbacks: Settings > Discussion - reduces unnecessary database writes
- Optimize autoloaded options: Run
SELECT option_name, length(option_value) FROM wp_options WHERE autoload='yes' ORDER BY length(option_value) DESC LIMIT 20;to find bloated options - Enable full-page caching: WP Super Cache or W3 Total Cache serves cached HTML to anonymous users without executing PHP
A well-optimized WordPress site with Redis object caching and full-page caching can serve thousands of concurrent anonymous visitors from a modest server.