“Premature optimization is the root of all evil but lack of optimisation is the root of outages.”
Table of Contents
- Introduction
- Architecture Overview
- Why Optimisation Is Required
- Nginx Optimisations & Parameter Calculations
- PHP-FPM Optimisations & Parameter Calculations
- MySQL Optimisations & Parameter Calculations
- System-Level Optimisations (Linux)
- Practical Example: Small vs Medium vs Large Server
- Interesting Facts & Statistics
- FAQs
- Key Takeaways
- Conclusion
1. Introduction
Modern web applications rely heavily on the Nginx + PHP + MySQL (LEMP) stack. While default configurations work for testing, they are not suitable for production traffic.
Optimisation ensures:
- Faster page load time
- Better concurrency handling
- Lower memory and CPU usage
- Higher stability under load
This document explains what to optimise, why to optimise, and how to calculate parameters practically.
2. Architecture Overview
A typical request flow:
- Client sends HTTP request
- Nginx handles connection & static content
- PHP-FPM processes dynamic PHP requests
- MySQL serves data from database
- Response sent back to client
Each layer must be tuned together, not individually.
3. Why Optimisation Is Required
Default settings:
- Are conservative
- Waste available RAM
- Limit concurrency
- Cause slow response under load
Common Problems Without Optimisation
- 502 / 504 Gateway errors
- High CPU load
- PHP-FPM “server reached max_children”
- MySQL “Too many connections”
4. Nginx Optimisations & Parameter Calculations
Key Nginx Parameters
worker_processes auto;
worker_connections 4096;
What is worker_processes
Number of worker processes
Best practice: match CPU cores
nproc
Example:
4 CPU cores → worker_processes 4;
What is worker_connections
Maximum connections per worker
Total Max Connections
worker_processes × worker_connections
Example
4 × 4096 = 16,384 concurrent connections
Recommended Extra Optimisations
use epoll;
multi_accept on;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
5. PHP-FPM Optimisations & Parameter Calculations
Key PHP-FPM Settings
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
How to Calculate pm.max_children
- Find average PHP process memory: ps -ylC php-fpm --sort:rss
- Formula: Available RAM for PHP / Avg PHP process size
Example
- Available RAM: 2 GB
- Avg PHP process: 100 MB
2048 / 100 ≈ 20
pm.max_children = 20
Other Important PHP Optimisations
request_terminate_timeout = 60
max_execution_time = 60
memory_limit = 256M
Enable OPcache
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
6. MySQL Optimisations & Parameter Calculations
Key MySQL Parameters
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
max_connections = 200
How to Calculate innodb_buffer_pool_size
- Allocate 60–70% of total RAM (dedicated DB server)
Example
- Server RAM: 4 GB
4 × 70% ≈ 2.8 GB
Use 2G or 3G
Connection Calculation
Additional Recommended Settings
query_cache_type = 0
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
7. System-Level Optimisations (Linux)
File Descriptors
ulimit -n 100000
Kernel Tuning
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
vm.swappiness = 10
8. Practical Server Size Examples
Small Server (2 CPU / 2 GB RAM)
- Nginx workers: 2
- worker_connections: 2048
- PHP max_children: 10
- MySQL buffer pool: 1G
Medium Server (4 CPU / 8 GB RAM)
- Nginx workers: 4
- worker_connections: 4096
- PHP max_children: 30–40
- MySQL buffer pool: 4–5G
Large Server (8 CPU / 16 GB RAM)
- Nginx workers: 8
- worker_connections: 8192
- PHP max_children: 60–80
- MySQL buffer pool: 10–12G
Practical Demonstration (Images Explained)
Step 1. NGINX OPTIMISATION
Parameter Calculations
Step 1.1 Backup current nginx.conf
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
Step 1.2 Check current config
cat /etc/nginx/nginx.conf
Screenshot: Current state before changes
Step 1.3 Edit nginx.conf
sudo nano /etc/nginx/nginx.conf
Replace/update with this optimized config:
user www-data;
worker_processes 2; # = nproc (2 cores)
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024; # 2 x 1024 = 2048 total connections
use epoll; # Linux best event model
multi_accept on; # Accept multiple connections at once
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30; # Reduced from default 75s
keepalive_requests 100;
types_hash_max_size 2048;
server_tokens off; # Hide nginx version
client_max_body_size 20m;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Settings
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn; # Only warn+ to reduce I/O
# Gzip Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 3; # Level 3 = good ratio, low CPU
gzip_min_length 1024; # Don't compress tiny files
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
# Open File Cache
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# FastCGI Cache (optional - enable per site)
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=PHPCACHE:10m
max_size=100m inactive=60m use_temp_path=off;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Step 1.4 Create FastCGI cache directory
sudo mkdir -p /var/cache/nginx
sudo chown www-data:www-data /var/cache/nginx
Step 1.5 Test and reload Nginx
sudo nginx -t
sudo systemctl reload nginx
Screenshot: nginx -t showing syntax is ok and test is successful
Step 1.6 Verify Nginx is running with new settings
sudo nginx -T | grep -E "worker_processes|worker_connections|gzip|keepalive_timeout"
systemctl status nginx
Screenshot: Running status + key parameters
Step 2. PHP-FPM OPTIMISATION
Parameter Calculations
Available RAM for PHP-FPM: ~150MB (conservative, leaving room for MySQL + Nginx)
Average PHP-FPM process size: ~30-40MB
Formula: pm.max_children = 150 / 35 ≈ 4
PHP-FPM Parameters Calculation
pm
Formula: Dynamic (best for variable traffic)
Value: dynamic
pm.max_children
Formula: 150MB ÷ 35MB/process
Value: 4
pm.start_servers
Formula: pm.max_children / 2
Value: 2
pm.min_spare_servers
Formula: pm.start_servers / 2
Value: 1
pm.max_spare_servers
Formula: pm.start_servers
Value: 2
pm.max_requests
Formula: Prevent memory leaks
Value: 500
Step 2.1 Check PHP-FPM version path
php -v
ls /etc/php/
Step 2.2 Backup pool config
sudo cp/etc/php/8.4/fpm/pool.d/www.conf/etc /php/8.4/fpm/pool.d/www.conf.bak
sudo cp /etc/php/8.4/fpm/php.ini /etc/php/8.4/fpm/php.ini.bak
Step 2.3 Check current PHP process memory usage
- Run this to see actual PHP-FPM process sizes
ps aux | grep php-fpm | grep -v grep | awk '{sum += $6} END {print "Total RSS:", sum/1024, "MB"; print "Count:", NR; print "Avg per process:", sum/NR/1024, "MB"}'
Screenshot: Current process sizes.
Step 2.4 Edit PHP-FPM pool config
sudo nano /etc/php/8.4/fpm/pool.d/www.conf
Find and update these values (search with Ctrl+W in nano):
[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 2
pm.max_requests = 500 ;Restart workers after 500 requests (prevents memory leaks)
pm.process_idle_timeout = 10s
pm.status_path = /status ; Enable FPM status page
ping.path = /ping
slowlog = /var/log/php8.4-fpm-slow.log
request_slowlog_timeout = 5s ; Log requests taking > 5 seconds
security.limit_extensions = .php
Step 2.5 Tune PHP OPcache
sudo nano /etc/php/8.4/mods-available/opcache.ini
zend_extension=opcache
; Enable OPcache
opcache.enable=1
opcache.enable_cli=0
; Memory: 64MB for low-RAM server
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
; Production settings (set validate_timestamps=0 in prod)
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.max_wasted_percentage=10
opcache.use_cwd=1
; JIT (PHP 8.x feature)
opcache.jit_buffer_size=32M
opcache.jit=1255
Step 2.6 Tune PHP.ini key values
sudo nano /etc/php/8.4/fpm/php.ini
Find and update:
; Memory limit per PHP process
memory_limit = 128M
; Upload/POST limits
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 60
max_input_time = 60
; Error handling (production)
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
; Session handling
session.gc_maxlifetime = 1440
session.cookie_httponly = 1
session.cookie_secure = 1
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
Step 2.7 Restart PHP-FPM and verify
sudo php-fpm8.4 -t
sudo systemctl restart php8.4-fpm
sudo systemctl status php8.4-fpm
Screenshot: PHP-FPM status showing active (running)
Step 2.8 Verify OPcache is active
php -r "var_dump(opcache_get_status());" | head -30
- or check via CLI
php -i | grep -E "opcache|OPcache"
Screenshot: OPcache enabled status
Step 2.9 Check PHP-FPM processes after restart
ps aux | grep php-fpm | grep -v grep
- Should show master + 2 worker processes (pm.start_servers=2)
Screenshot: FPM process list
Step 3. MYSQL OPTIMISATION
Parameter Calculations
RAM budget for MySQL: ~200MB (out of 914MB total)
innodb_buffer_pool_size
Formula: ~20% of RAM (shared server)
Value: 192M
innodb_buffer_pool_instances
Formula: buffer_pool ÷ 128M
Value: 1
max_connections
Formula: Low RAM = conservative
Value: 50
innodb_log_file_size
Formula: 25% of buffer pool
Value: 48M
tmp_table_size
Formula: Memory temporary tables
Value: 16M
max_heap_table_size
Formula: Same as tmp_table_size
Value: 16M
thread_cache_size
Formula: Reuse threads
Value: 8
table_open_cache
Formula: Open tables cache
Value: 400
Step 3.1 Check current MySQL config and status
sudo cp /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/mysqld.cnf.bak
- Check current variables
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool%';"
mysql -u root -p -e "SHOW VARIABLES LIKE 'max_connections';"
mysql -u root -p -e "SHOW STATUS LIKE 'Threads_connected';"
Screenshot: Current MySQL variables
Step 3.2 Check actual MySQL memory usage
mysql -u root -p -e "
SELECT
ROUND(@@innodb_buffer_pool_size/1024/1024, 0) AS 'Buffer Pool MB',
ROUND(@@key_buffer_size/1024/1024, 0) AS 'Key Buffer MB',
@@max_connections AS 'Max Connections',
@@thread_stack/1024 AS 'Thread Stack KB';
"
Screenshot: Current MySQL memory config
Step 3.3 Edit MySQL config
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Add/update under [mysqld]:
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
log-error = /var/log/mysql/error.log
#
# ===== MEMORY SETTINGS =====
# Server has 914MB RAM - allocate ~200MB for MySQL
#
innodb_buffer_pool_size = 192M # Main InnoDB cache (most important!)
innodb_buffer_pool_instances = 1 # 1 instance (< 1GB pool)
innodb_log_file_size = 48M # ~25% of buffer pool
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 2 # Slight risk, big perf gain (use 1 for strict ACID)
#
# ===== CONNECTION SETTINGS =====
#
max_connections = 50 # Low RAM = keep connections limited
thread_cache_size = 8 # Reuse threads, avoid creation overhead
wait_timeout = 120 # Kill idle connections after 2 min
interactive_timeout = 120
#
# ===== QUERY CACHE (removed in MySQL 8.0, skip) =====
# MySQL 8.0 removed query_cache - use ProxySQL or app-level cache
#
# ===== TABLE CACHE =====
#
table_open_cache = 400
table_definition_cache = 400
#
# ===== TEMP TABLES =====
#
tmp_table_size = 16M
max_heap_table_size = 16M
#
# ===== InnoDB I/O =====
#
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT # Avoid double buffering with OS cache
innodb_read_io_threads = 2 # = CPU cores
innodb_write_io_threads = 2 # = CPU cores
#
# ===== SLOW QUERY LOG =====
#
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2 # Log queries > 2 seconds
log_queries_not_using_indexes = 1
#
# ===== BINARY LOG (disable if not using replication) =====
#
# skip-log-bin # Uncomment if no replication needed
Step 3.4 Validate and restart MySQL
sudo mysqld --validate-config
sudo systemctl restart mysql
sudo systemctl status mysql
Screenshot: MySQL status active
Step 3.5 Verify new MySQL variables
mysql -u root -p -e "
SELECT 'innodb_buffer_pool_size' AS Variable,
ROUND(@@innodb_buffer_pool_size/1024/1024,0) AS 'Value (MB)'
UNION SELECT 'max_connections', @@max_connections
UNION SELECT 'innodb_log_file_size MB', ROUND(@@innodb_log_file_size/1024/1024,0)
UNION SELECT 'tmp_table_size MB', ROUND(@@tmp_table_size/1024/1024,0)
UNION SELECT 'thread_cache_size', @@thread_cache_size;
"
Screenshot: New values confirmed
Step 3.6 Check MySQL memory usage post-restart
free -h
ps aux | sort -k6 -rn | head -10
Screenshot: Overall memory after all services tuned
“Measure first, tune second, and monitor always.”
Step 4. FINAL VERIFICATION
Step 4.1 Check all services running
systemctl status nginx php8.4-fpm mysql --no-pager
Step 4.2 Full memory picture
free -h
ps aux --sort=-%mem | head -15
Step 4.3 Check nginx + PHP working together
Test nginx config is still valid
sudo nginx -t
Test PHP-FPM socket exists
ls -la /run/php/php8.4-fpm.sock
Check FPM status (if you added status page to your site config)
curl http://localhost/status
Step 4.4 Check MySQL slow log is active
mysql -u root -p -e "SHOW VARIABLES LIKE 'slow_query%';"
Step 4.5 Final health summary
echo "=== NGINX ===" && nginx -v && systemctl is-active nginx
echo "=== PHP-FPM ===" && php -v | head -1 && systemctl is-active php8.4-fpm
echo "=== MYSQL ===" && mysql --version && systemctl is-active mysql
echo "=== MEMORY ===" && free -h
Screenshot: Final health check
Your server is fully optimised. All phases verified:
- Phase 0 Swap 2GB active
- Phase 1 Nginx tuned
- Phase 2 PHP-FPM + OPcache + JIT enabled
- Phase 3 MySQL InnoDB / connections / slow query log active
9. Interesting Facts & Statistics
- 1 second delay can reduce conversions by 7%
- OPcache can improve PHP performance by 2–3×
- MySQL buffer pool cache hit ratio above 99% is ideal
- Nginx handles 10× more concurrent connections than traditional servers
11. FAQs
Q1. Should I optimise Nginx, PHP, or MySQL first?
Start with PHP-FPM and MySQL, then tune Nginx.
Q2. Can wrong tuning crash the server?
Yes. Over-allocating RAM causes OOM kills.
Q3. Are these values fixed forever?
No. Recalculate after traffic growth.
Q4. Do I need load testing?
Yes. Use tools like ab, wrk, or k6.
12. Key Takeaways
- Optimisation is calculation-based, not guesswork
- PHP-FPM memory calculation is critical
- MySQL buffer pool has the biggest performance impact
- Nginx handles concurrency, not application logic
- Monitoring is mandatory after tuning
13. Conclusion
Optimising Nginx + PHP + MySQL is not about copying configs from the internet—it is about understanding server resources, calculating limits, and balancing load across layers.
A well-optimised stack:
- Handles higher traffic
- Reduces downtime
- Improves user experience
- Saves infrastructure cost
About the Author:Narendra is a DevOps Engineer at AddWebSolution, specializing in automating infrastructure to improve efficiency and reliability.













Top comments (0)