• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on AWS for WordPress

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MySQL on AWS for WordPress

Nginx: The Frontend Gatekeeper

Nginx serves as the primary entry point for all WordPress traffic. Its configuration is critical for efficient request handling, SSL termination, caching, and load balancing. On AWS, we’ll typically deploy Nginx on an EC2 instance or leverage AWS Elastic Load Balancing (ELB) with Nginx as a backend. This section focuses on tuning Nginx itself.

Core Nginx Directives for WordPress

The `nginx.conf` file, often located at `/etc/nginx/nginx.conf` or within `/etc/nginx/conf.d/`, is where we’ll make our adjustments. Key areas include the `events` and `http` blocks.

`events` Block Tuning

This block controls how Nginx handles connections. For high-traffic WordPress sites, optimizing worker connections is paramount.

`worker_connections`

This directive sets the maximum number of simultaneous connections that can be opened by a single worker process. The theoretical maximum is limited by the system’s file descriptor limit. A good starting point for a moderately busy site is 1024, but this can be scaled up significantly.

`multi_accept`

Setting `multi_accept on;` allows a worker process to accept as many new connections as possible, rather than processing them one at a time. This can improve performance under heavy load.

`use epoll;`

On Linux systems, `epoll` is the most scalable event notification mechanism. Ensure this is set for optimal performance.

`http` Block Tuning

This block contains directives that affect the entire HTTP service. We’ll focus on caching, keepalive, and buffer sizes.

`sendfile` and `tcp_nopush`/`tcp_nodelay`

These directives optimize the transfer of files. `sendfile on;` allows Nginx to send files directly from the kernel’s page cache, reducing overhead. `tcp_nopush on;` tells Nginx to try and send HTTP response headers in one packet. `tcp_nodelay on;` disables the Nagle algorithm, which can reduce latency for small packets.

`keepalive_timeout`

This directive sets the timeout for persistent connections. A lower value (e.g., 65 seconds) can free up resources faster, while a higher value can improve performance for clients making multiple requests. Experimentation is key here.

`client_body_buffer_size` and `client_max_body_size`

These are crucial for handling file uploads (e.g., media in WordPress). `client_body_buffer_size` is the buffer size for reading client request body. `client_max_body_size` sets the maximum allowed size of the client request body. For WordPress, a common value is `client_max_body_size 64m;` or higher, depending on typical upload sizes.

Buffering Directives (`proxy_buffers`, `proxy_buffer_size`, `proxy_read_timeout`)

When Nginx acts as a reverse proxy to Gunicorn/FPM, these directives become vital. They control how Nginx buffers responses from the backend. Insufficient buffer sizes can lead to 502 Bad Gateway errors or slow response times. A common starting point for `proxy_buffers` is `16 4k` or `32 8k`, and `proxy_buffer_size` can be set to `16k` or `32k`.

Example Nginx Configuration Snippet

Here’s a consolidated example for the `http` block, assuming Nginx is proxying to a Gunicorn/FPM backend. Adjust `worker_processes` to match your CPU cores.

`nginx.conf` Snippet

worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 4096;
    multi_accept on;
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip compression for static assets and HTML
    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;

    # Buffering for proxying to backend (Gunicorn/FPM)
    proxy_buffers 16 16k;
    proxy_buffer_size 32k;
    proxy_read_timeout 300s; # Increased timeout for potentially long-running PHP scripts or Gunicorn requests
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;

    # Client body settings for uploads
    client_body_buffer_size 128k;
    client_max_body_size 64m; # Adjust as needed for WordPress media uploads

    # Include other configurations
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # SSL configuration (example)
    # ssl_protocols TLSv1.2 TLSv1.3;
    # ssl_prefer_server_ciphers on;
    # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # WordPress specific location block (example)
    server {
        listen 80;
        # listen 443 ssl http2; # Uncomment for SSL
        server_name your_domain.com www.your_domain.com;

        # root /var/www/html/wordpress; # Path to your WordPress installation
        # index index.php index.html index.htm;

        # Access logs
        access_log /var/log/nginx/wordpress_access.log;
        error_log /var/log/nginx/wordpress_error.log;

        # Deny access to hidden files
        location ~ /\. {
            deny all;
        }

        # Serve static files directly
        location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2|ttf|eot|otf)$ {
            expires 30d;
            add_header Cache-Control "public, no-transform";
            try_files $uri =404;
        }

        # PHP processing (for FPM)
        location ~ \.php$ {
            try_files $uri =404;
            include snippets/fastcgi-php.conf;
            # For PHP-FPM
            fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }

        # Proxying to Gunicorn (for Python-based WordPress alternatives or headless CMS)
        # location / {
        #     proxy_pass http://your_gunicorn_backend_ip:8000; # Replace with your Gunicorn endpoint
        #     proxy_set_header Host $host;
        #     proxy_set_header X-Real-IP $remote_addr;
        #     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        #     proxy_set_header X-Forwarded-Proto $scheme;
        # }

        # WordPress permalinks
        location / {
            try_files $uri $uri/ /index.php?$args;
        }
    }
}

Applying Changes and Monitoring

After modifying `nginx.conf`, always test the configuration before reloading:

sudo nginx -t

If the test passes, reload Nginx to apply the changes:

sudo systemctl reload nginx

Monitor Nginx access and error logs (`/var/log/nginx/access.log`, `/var/log/nginx/error.log`) for any unusual activity or errors. Tools like `netstat -anp | grep nginx` or `ss -s` can provide insights into active connections.

Gunicorn/PHP-FPM: The Application Server

This layer is responsible for executing the WordPress PHP code or serving dynamic content if using a Python-based backend with Gunicorn. Tuning here directly impacts application response times and resource utilization.

PHP-FPM Tuning

PHP-FPM (FastCGI Process Manager) is the standard for running PHP applications. Its configuration is typically found in `/etc/php/[version]/fpm/php.ini` and `/etc/php/[version]/fpm/pool.d/www.conf`.

`php.ini` Directives

While many `php.ini` settings affect PHP execution globally, some are particularly relevant for performance:

  • memory_limit: The maximum amount of memory a script can consume. For WordPress, this often needs to be increased, e.g., 256M or 512M.
  • max_execution_time: The maximum time in seconds a script is allowed to run. Default is 30s, which can be too low for complex WordPress operations. Increase to 120 or 300.
  • upload_max_filesize and post_max_size: Should align with Nginx’s client_max_body_size.
  • opcache.enable=1: Essential for performance. Ensure OPcache is enabled and configured.
  • opcache.memory_consumption: Amount of memory for storing compiled PHP code. 128 or 256 MB is a good start.
  • opcache.interned_strings_buffer: For storing interned strings. 16 MB is often sufficient.
  • opcache.max_accelerated_files: Maximum number of files to cache. 10000 or more is recommended for larger sites.
  • opcache.revalidate_freq: How often to check for file updates. 0 (never revalidate) or a high value (e.g., 60 seconds) can improve performance by reducing filesystem checks, but increases the risk of serving stale code if not managed carefully.

`www.conf` (PHP-FPM Pool Configuration)

This file controls the PHP-FPM worker processes. The key is to balance the number of processes with available server resources.

Process Management (`pm`)

PHP-FPM offers three process management modes:

  • static: A fixed number of child processes are spawned when the pool starts and remain active. Best for predictable, high-load environments.
  • dynamic: The number of child processes varies between pm.min_spare_servers and pm.max_spare_servers, with a target of pm.max_children. Good for fluctuating loads.
  • ondemand: Processes are spawned only when a request arrives and are terminated after a period of inactivity. Saves resources but can introduce latency on initial requests.

For WordPress, dynamic or static are generally preferred. If using dynamic, tune the spare server counts carefully.

Tuning `pm.max_children`

This is the most critical directive. It defines the maximum number of child processes that can run simultaneously. Each PHP-FPM process consumes memory. A common formula to estimate is:

pm.max_children = (Total RAM - RAM for OS/Nginx/MySQL) / Average RAM per PHP-FPM process

Average RAM per PHP-FPM process can be estimated by monitoring a few idle processes using `ps aux | grep php-fpm` and taking an average. For a 16GB EC2 instance, you might start with pm.max_children = 100 if Nginx and MySQL are also running on the same instance and are well-tuned.

Other `pm` Directives

If using dynamic:

  • pm.min_spare_servers: Minimum number of idle processes.
  • pm.max_spare_servers: Maximum number of idle processes.
  • pm.start_servers: Number of processes started when the pool starts.

Example PHP-FPM Pool Configuration (`www.conf`)

; /etc/php/8.1/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /var/run/php/php8.1-fpm.sock ; Or a TCP socket like 127.0.0.1:9000
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 150       ; Adjust based on server RAM and other services
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.start_servers = 5
pm.max_requests = 500       ; Restart processes after this many requests to prevent memory leaks

; Request handling settings
request_terminate_timeout = 120s ; Corresponds to max_execution_time in php.ini, but FPM specific
; request_slowlog_timeout = 10s ; Enable slow log for debugging

; Other settings
; rlimit_files = 1024
; rlimit_nofile = 65536
; chroot = /var/www/html/wordpress ; If you need chrooting
; access.log = /var/log/php/php8.1-fpm.access.log
; slowlog = /var/log/php/php8.1-fpm.slow.log

Gunicorn Tuning (for Python-based Backends)

If your WordPress setup uses a Python framework (e.g., Django, Flask) with Gunicorn as the WSGI server, tuning is different. Gunicorn’s performance is heavily influenced by its worker count and type.

Worker Processes

Gunicorn typically uses:

  • sync (synchronous): The default. Each worker handles one request at a time.
  • gevent / event (asynchronous): Workers can handle multiple requests concurrently using non-blocking I/O. Recommended for I/O-bound applications.

The number of workers is often set as:

WEB_CONCURRENCY = (2 * Number of CPU Cores) + 1

This is a common heuristic for synchronous workers. For asynchronous workers (like gevent), you might use a higher number, but monitor resource usage closely.

Example Gunicorn Command Line

gunicorn --workers 4 --worker-class gevent --bind 0.0.0.0:8000 myapp.wsgi:application --timeout 120 --graceful-timeout 60

Here:

  • --workers 4: Sets the number of worker processes.
  • --worker-class gevent: Uses the gevent worker class for concurrency.
  • --bind 0.0.0.0:8000: Listens on all interfaces on port 8000.
  • --timeout 120: Sets the worker timeout to 120 seconds.
  • --graceful-timeout 60: Time to wait for graceful shutdown.

Applying Changes and Monitoring

After modifying PHP-FPM configuration files, restart the service:

sudo systemctl restart php8.1-fpm # Adjust version as needed

For Gunicorn, you’ll typically restart the systemd service or supervisor process that manages it.

Monitor PHP-FPM logs (often in /var/log/php/) and Gunicorn logs for errors. Use tools like htop or top to observe CPU and memory usage of php-fpm processes or gunicorn workers.

MySQL: The Data Backbone

The MySQL database is where WordPress stores all its content, settings, and user data. Optimizing MySQL can yield significant performance gains, especially for read-heavy workloads common with WordPress.

Key MySQL Configuration (`my.cnf` or `mysqld.cnf`)

The main configuration file is typically located at `/etc/mysql/my.cnf`, `/etc/mysql/mysql.conf.d/mysqld.cnf`, or similar. We’ll focus on InnoDB settings, as it’s the default and recommended storage engine for WordPress.

`innodb_buffer_pool_size`

This is arguably the most important setting for InnoDB performance. It’s the memory area where InnoDB caches table data and indexes. For a dedicated database server, setting this to 70-80% of available RAM is common. If MySQL runs on the same instance as Nginx/PHP-FPM, allocate a smaller portion, e.g., 4-8GB on a 16GB instance.

`innodb_log_file_size` and `innodb_log_buffer_size`

These affect write performance. Larger log files can improve write throughput but increase recovery time after a crash. A common starting point is innodb_log_file_size = 256M or 512M. innodb_log_buffer_size (e.g., 16M) is for buffering transactions before writing to the log files.

`innodb_flush_log_at_trx_commit`

Controls durability vs. performance.

  • 1 (default): ACID compliant. Log buffer is written to the log file, and the log file is flushed to disk on every commit. Safest but slowest.
  • 0: Log buffer is written to the log file once per second, and the log file is flushed to disk once per second. Faster, but you can lose up to 1 second of transactions in a crash.
  • 2: Log buffer is written to the log file on every commit, but the log file is flushed to disk once per second. A good compromise for performance and safety.
For WordPress, 2 is often a good balance. If data loss is absolutely critical, stick with 1.

`innodb_flush_method`

Determines how data and log files are flushed to disk. On Linux, O_DIRECT is often recommended to bypass the OS file system cache and avoid double buffering, especially when innodb_buffer_pool_size is large. However, test this as it can sometimes perform worse depending on the underlying storage.

`max_connections`

The maximum number of simultaneous client connections. WordPress plugins can sometimes open many connections. A value between 150-300 is often suitable for a moderately busy site. Monitor actual usage.

Query Cache (Deprecated/Removed)

Note: The MySQL Query Cache has been deprecated since 5.7 and removed in MySQL 8.0. If you are on an older version, it might be enabled, but it often causes more problems than it solves with dynamic content like WordPress. It’s generally recommended to disable it.

Example MySQL Configuration Snippet

[mysqld]
# General Settings
user                    = mysql
pid-file                = /var/run/mysqld/mysqld.pid
socket                  = /var/run/mysqld/mysqld.sock
port                    = 3306
basedir                 = /usr
datadir                 = /var/lib/mysql
tmpdir                  = /tmp
lc-messages-dir         = /usr/share/mysql
skip-external-locking

# InnoDB Settings
default_storage_engine  = InnoDB
innodb_buffer_pool_size = 8G       ; Adjust based on available RAM (e.g., 70% of RAM if dedicated DB server)
innodb_log_file_size    = 512M
innodb_log_buffer_size  = 16M
innodb_flush_log_at_trx_commit = 2 ; Compromise between performance and durability
innodb_flush_method     = O_DIRECT ; Test this setting

# Connection Settings
max_connections         = 250    ; Adjust based on application needs and server capacity
# thread_cache_size     = 16     ; Cache threads for reuse

# Other Performance Settings
query_cache_type        = 0      ; Disable query cache (deprecated/removed in newer versions)
query_cache_size        = 0

# Logging (Optional, but useful for debugging)
# log_error = /var/log/mysql/error.log
# slow_query_log = 1
# slow_query_log_file = /var/log/mysql/mysql-slow.log
# long_query_time = 2

Applying Changes and Monitoring

After modifying the MySQL configuration file, restart the MySQL service:

sudo systemctl restart mysql # Or mysqld, depending on your OS

Verify the new settings are loaded:

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

Monitor MySQL performance using tools like mysqltuner.pl (a script that analyzes your MySQL server and provides tuning recommendations), Percona Monitoring and Management (PMM), or AWS CloudWatch metrics for RDS instances. Pay attention to Threads_connected, Slow_queries, and buffer pool hit rate.

AWS Specific Considerations

When deploying this stack on AWS, several factors come into play:

EC2 Instance Sizing

Choose instance types that balance CPU, RAM, and Network I/O. For WordPress, general-purpose instances (e.g., `m5`, `m6g`) are often suitable. Database-specific instances (`db.rX`, `db.mX`) are ideal if using AWS RDS.

EBS Volume Performance

For the EC2 instance hosting MySQL, use Provisioned IOPS SSD (io1/io2) or General Purpose SSD (gp3) volumes. gp3 volumes offer independent scaling of IOPS and throughput, making them a cost-effective choice. Ensure sufficient IOPS are provisioned for your database workload.

AWS RDS

If using AWS Relational Database Service (RDS) for MySQL, many of these tuning parameters are managed via DB Parameter Groups. You can create custom parameter groups to override default settings like innodb_buffer_pool_size, max_connections, innodb_flush_log_at_trx_commit, etc. Ensure you select an appropriate instance class and storage type for your RDS instance.

Elastic Load Balancing (ELB)

If using an ELB (Application Load Balancer or Network Load Balancer) in front of your Nginx instances, configure health checks appropriately. For backend Nginx servers, ensure ELB idle timeouts are longer than your application’s expected response times, or configure Nginx to send keepalive packets to prevent premature connection closure.

Caching Strategies

Beyond Nginx’s built-in caching, consider:

  • Object Caching: Use Redis or Memcached (via plugins like W3 Total Cache or LiteSpeed Cache) to cache database query results and objects, significantly reducing MySQL load.
  • CDN: Utilize AWS CloudFront or a third-party CDN to serve static assets (images, CSS, JS) from edge locations, reducing load on your origin servers.
  • Page Caching: WordPress caching plugins (e.g., WP Super Cache, W3 Total Cache, WP Rocket) generate static HTML files of your pages, which Nginx can serve directly, bypassing PHP/MySQL entirely for most requests.

Conclusion

Tuning Nginx, Gunicorn/PHP-FPM, and MySQL is an iterative process. Start with these baseline configurations, monitor your application’s performance under realistic load, and adjust parameters based on observed bottlenecks. AWS provides the infrastructure, but meticulous configuration of each layer is what truly unlocks optimal performance and scalability for your WordPress deployment.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala