DEV Community

Cover image for Nginx + PHP + MySQL Optimisations and Parameter Calculations

Nginx + PHP + MySQL Optimisations and Parameter Calculations

“Premature optimization is the root of all evil but lack of optimisation is the root of outages.”

Table of Contents

  1. Introduction
  2. Architecture Overview
  3. Why Optimisation Is Required
  4. Nginx Optimisations & Parameter Calculations
  5. PHP-FPM Optimisations & Parameter Calculations
  6. MySQL Optimisations & Parameter Calculations
  7. System-Level Optimisations (Linux)
  8. Practical Example: Small vs Medium vs Large Server
  9. Interesting Facts & Statistics
  10. FAQs
  11. Key Takeaways
  12. 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:

  1. Client sends HTTP request
  2. Nginx handles connection & static content
  3. PHP-FPM processes dynamic PHP requests
  4. MySQL serves data from database
  5. 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

How to Calculate pm.max_children

  1. Find average PHP process memory: ps -ylC php-fpm --sort:rss
  2. 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
Enter fullscreen mode Exit fullscreen mode

Enable OPcache

opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
Enter fullscreen mode Exit fullscreen mode

6. MySQL Optimisations & Parameter Calculations

Key MySQL Parameters

innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
max_connections = 200
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 1.2 Check current config

  cat /etc/nginx/nginx.conf
Enter fullscreen mode Exit fullscreen mode

Screenshot: Current state before changes

Step 1.3 Edit nginx.conf

sudo nano /etc/nginx/nginx.conf
Enter fullscreen mode Exit fullscreen mode

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/*;
  }
Enter fullscreen mode Exit fullscreen mode

Step 1.4 Create FastCGI cache directory

 sudo mkdir -p /var/cache/nginx
  sudo chown www-data:www-data /var/cache/nginx
Enter fullscreen mode Exit fullscreen mode

Step 1.5 Test and reload Nginx

sudo nginx -t
  sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

Screenshot: Current process sizes.

Step 2.4 Edit PHP-FPM pool config

sudo nano /etc/php/8.4/fpm/pool.d/www.conf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Screenshot: PHP-FPM status showing active (running)

Step 2.8 Verify OPcache is active

php -r "var_dump(opcache_get_status());" | head -30
Enter fullscreen mode Exit fullscreen mode

- or check via CLI

php -i | grep -E "opcache|OPcache"
Enter fullscreen mode Exit fullscreen mode

Screenshot: OPcache enabled status

Step 2.9 Check PHP-FPM processes after restart

  ps aux | grep php-fpm | grep -v grep
Enter fullscreen mode Exit fullscreen mode

- 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
Enter fullscreen mode Exit fullscreen mode

- 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';"
Enter fullscreen mode Exit fullscreen mode

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';
  "
Enter fullscreen mode Exit fullscreen mode

Screenshot: Current MySQL memory config

Step 3.3 Edit MySQL config

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 3.4 Validate and restart MySQL

sudo mysqld --validate-config
  sudo systemctl restart mysql
  sudo systemctl status mysql
Enter fullscreen mode Exit fullscreen mode

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;
  "
Enter fullscreen mode Exit fullscreen mode

Screenshot: New values confirmed

Step 3.6 Check MySQL memory usage post-restart

  free -h
  ps aux | sort -k6 -rn | head -10
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4.2 Full memory picture

free -h
  ps aux --sort=-%mem | head -15
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4.4 Check MySQL slow log is active

mysql -u root -p -e "SHOW VARIABLES LIKE 'slow_query%';"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)