• 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 Redis on AWS for C

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and Redis on AWS for C

Nginx as a High-Performance Frontend Proxy

Nginx is the de facto standard for serving static assets and acting as a reverse proxy for dynamic applications. For optimal performance, especially under heavy load, fine-tuning Nginx’s worker processes, connection handling, and caching mechanisms is crucial. We’ll focus on a common AWS setup where Nginx sits in front of Gunicorn (for Python apps) or PHP-FPM (for PHP apps) and Redis.

Worker Processes and Connections

The `worker_processes` directive controls how many worker processes Nginx spawns. A common recommendation is to set this to the number of CPU cores available to the Nginx instance. The `worker_connections` directive limits the number of simultaneous connections a single worker process can handle. The total maximum connections will be `worker_processes * worker_connections`.

On an EC2 instance, you can determine the number of CPU cores using the `nproc` command or by inspecting the instance type’s specifications. For example, a `t3.medium` instance typically has 2 vCPUs.

Tuning `nginx.conf`

Locate your `nginx.conf` file (often at `/etc/nginx/nginx.conf` or `/usr/local/nginx/conf/nginx.conf`). Adjust the `events` and `http` blocks as follows:

user www-data;
worker_processes auto; # Or set to the number of CPU cores, e.g., 2
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024; # Adjust based on expected load and server resources
    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 for text-based assets
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Include other configurations
    include /etc/nginx/mime.types;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Explanation:

  • worker_processes auto;: Lets Nginx automatically determine the number of worker processes based on CPU cores.
  • worker_connections 1024;: A reasonable starting point. Monitor your server’s connection usage and adjust upwards if necessary, but be mindful of system limits (e.g., `/proc/sys/net/core/somaxconn`).
  • sendfile on;: Efficiently transfers files from disk to network socket without user-space buffering.
  • tcp_nopush on;: Instructs Nginx to send header and file in one packet if possible.
  • tcp_nodelay on;: Disables the Nagle algorithm, which can reduce latency for small packets.
  • keepalive_timeout 65;: Keeps connections open for a specified duration, reducing overhead for subsequent requests.
  • server_tokens off;: Hides the Nginx version in HTTP headers, a minor security hardening step.
  • gzip on; ... gzip_types ...;: Enables and configures Gzip compression for text-based responses, significantly reducing bandwidth and improving load times.

Caching Strategies

Nginx can serve static assets directly from disk or cache them in memory or on disk for faster retrieval. For dynamic content, it can cache responses from backend applications.

Static Asset Caching

Configure browser caching and Nginx’s own caching for static files. In your site’s Nginx configuration (e.g., `/etc/nginx/sites-available/your_app`):

server {
    listen 80;
    server_name example.com;

    root /var/www/your_app/public;
    index index.php index.html index.htm;

    # Serve static files directly
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d; # Cache for 30 days in browser
        add_header Cache-Control "public, no-transform";
        access_log off; # Don't log static file access
        try_files $uri =404;
    }

    # Proxy to backend application
    location / {
        proxy_pass http://unix:/run/gunicorn.sock; # Or http://127.0.0.1:8000;
        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;
        proxy_read_timeout 300s; # Increase timeout for long-running requests
        proxy_connect_timeout 75s;
    }

    # ... other configurations like PHP-FPM if applicable
}

Backend Response Caching (with Redis)

For frequently accessed, non-user-specific dynamic content, Nginx can cache responses from your backend. This is particularly effective when combined with Redis for storing cache keys and invalidation signals.

http {
    # ... other http configurations

    # Define a cache zone
    proxy_cache_path /var/cache/nginx/my_app levels=1:2 keys_zone=my_app_cache:10m inactive=60m max_size=1g;
    # keys_zone=name:size - name for the zone, size of shared memory
    # inactive=time - how long an item can be unused before being removed
    # max_size=size - maximum size of the cache on disk

    # ... other http configurations

    server {
        listen 80;
        server_name example.com;

        # ... other server configurations

        location /api/ { # Cache API responses
            proxy_pass http://unix:/run/gunicorn.sock;
            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;

            # Cache settings
            proxy_cache my_app_cache; # Use the defined cache zone
            proxy_cache_valid 200 302 10m; # Cache successful responses for 10 minutes
            proxy_cache_valid 404 1m;    # Cache 404s for 1 minute
            proxy_cache_key "$scheme$request_method$host$request_uri"; # Unique cache key
            add_header X-Cache-Status $upstream_cache_status; # Useful for debugging

            # Bypass cache for authenticated users or specific requests
            proxy_cache_bypass $http_cookie; # Example: bypass if cookie is present
            proxy_no_cache $http_cookie;     # Example: don't store if cookie is present

            # If using Redis for cache invalidation, you'd typically handle this
            # within your application logic, sending a signal to Nginx or
            # directly invalidating cache entries via Nginx's cache manager module
            # or by setting appropriate Cache-Control headers.
            # For advanced invalidation, consider Nginx's Lua module or external tools.
        }

        # ... other location blocks
    }
}

Note on Redis Integration: Nginx itself doesn’t directly integrate with Redis for cache *storage* in the same way it uses its file-based cache. However, Redis is invaluable for:

  • Storing cache keys and metadata.
  • Implementing cache invalidation logic within your application. Your application can update Redis, and Nginx (or a custom module) can check Redis to determine if a cached item is stale or should be bypassed.
  • Rate limiting and session management, which indirectly affect caching decisions.

Gunicorn/PHP-FPM Tuning for Backend Performance

The application server (Gunicorn for Python, PHP-FPM for PHP) is where your application code executes. Optimizing its worker processes, memory usage, and request handling is critical.

Gunicorn (Python WSGI HTTP Server)

Gunicorn is a popular choice for deploying Python web applications. Its performance is heavily influenced by the number of worker processes and the worker type.

Worker Types

Gunicorn supports several worker types:

  • Sync (Synchronous): The default. Each worker handles one request at a time. Simple but can be a bottleneck under high concurrency.
  • Gevent/Eventlet: Asynchronous workers using green threads. Can handle many concurrent connections efficiently, especially for I/O-bound tasks. Requires installing `gevent` or `eventlet`.
  • Async (e.g., `aiohttp`): For applications built with `asyncio`.

For most CPU-bound or mixed workloads, a combination of multiple sync workers or gevent workers is effective. The number of workers is often set to `(2 * number_of_cores) + 1` as a starting point, but this can vary significantly based on application behavior.

Gunicorn Configuration (Command Line or `gunicorn.conf.py`)

You can configure Gunicorn via command-line arguments or a configuration file. Using a configuration file is cleaner for production.

# gunicorn.conf.py
import multiprocessing

# Number of worker processes. A common starting point is (2 * number_of_cores) + 1.
# For I/O bound tasks, consider using gevent workers and a higher number.
workers = multiprocessing.cpu_count() * 2 + 1
# workers = 3 # Example for a 1-core CPU

# Worker class. 'sync' is default. 'gevent' is good for I/O bound.
worker_class = 'sync' # or 'gevent'

# Bind to a Unix socket for Nginx to connect to (more efficient than TCP loopback)
# Ensure the user running Nginx has permissions to access this socket.
bind = "unix:/run/gunicorn.sock"
# Or for TCP: bind = "127.0.0.1:8000"

# Maximum number of requests a worker will handle before restarting.
# Helps prevent memory leaks.
max_requests = 1000
# Maximum number of requests a worker will handle before it's recycled.
# A value of 0 means never recycle.
# max_requests_jitter = 50 # Add some randomness to max_requests

# Timeout for worker requests.
# Increase this for long-running operations, but be mindful of Nginx proxy_read_timeout.
# Default is 30 seconds.
# timeout = 120

# Logging configuration
loglevel = 'info' # 'debug', 'info', 'warning', 'error', 'critical'
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'

# PID file
pidfile = '/run/gunicorn.pid'

# User and group to run as (if not running as root initially)
# user = 'your_app_user'
# group = 'your_app_group'

# Threads for async workers (if using 'gthread' worker class, not common)
# threads = 2

# Enable graceful reload
# reload = True # Use with caution in production, better managed by systemd/supervisor
# reload_engine = 'inotify' # or 'poll'

# Enable access log for requests that return an error
# log_syslog = True # Send logs to syslog
# syslog_handler = 'socket' # or 'udp' or 'tcp'
# syslog_address = '/dev/log' # or ('localhost', 514)

# Enable worker heartbeat
# worker_heartbeat = 30 # seconds

Deployment Example (using systemd):

# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target

[Service]
User=your_app_user
Group=your_app_group
WorkingDirectory=/path/to/your/app
ExecStart=/path/to/your/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/gunicorn.sock \
    --access-logfile /var/log/gunicorn/access.log \
    --error-logfile /var/log/gunicorn/error.log \
    --pid /run/gunicorn.pid \
    your_app.wsgi:application # Replace with your actual WSGI application entry point

# Optional: If using gevent workers, ensure gevent is installed and specify worker_class
# ExecStart=/path/to/your/venv/bin/gunicorn -k gevent -w 3 --bind unix:/run/gunicorn.sock ...

Restart=always
RestartSec=10

[Install]
[Install]
WantedBy=multi-user.target

After creating/modifying the service file, run:

sudo systemctl daemon-reload
sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn

PHP-FPM (FastCGI Process Manager)

PHP-FPM manages a pool of PHP processes that handle requests from the web server (Nginx). Its performance tuning involves configuring the process manager and worker pool.

Process Manager Settings

PHP-FPM offers different process management strategies:

  • Static: A fixed number of child processes are spawned when FPM starts. Best for predictable workloads and consistent performance.
  • Dynamic: FPM starts a few processes and spawns more as needed, up to a defined maximum. It can also kill idle processes to save resources. Good for variable workloads.
  • On-demand: Processes are spawned only when a request comes in and are killed after a timeout. Can save memory but might introduce latency for the first request.

For production, ‘static’ or ‘dynamic’ are generally preferred. ‘Static’ offers the most predictable performance. The number of processes should be tuned based on server CPU and memory, and the application’s resource consumption.

PHP-FPM Configuration (`php-fpm.conf` and Pool Configuration)**

The main configuration is typically in `/etc/php/X.Y/fpm/php-fpm.conf` (where X.Y is your PHP version), and pool-specific settings are in `/etc/php/X.Y/fpm/pool.d/www.conf` (or a custom pool file).

; /etc/php/X.Y/fpm/pool.d/www.conf

[www]
; Process management settings
; Choose one of the following process management modes:
; static, dynamic or ondemand
pm = dynamic

; If pm = dynamic, these are the values to use:
; pm.max_children: The maximum number of children that can be started.
; pm.start_servers: The number of children that will be started at FPM startup.
; pm.min_spare_servers: The minimum number of children that should be always available.
; pm.max_spare_servers: The maximum number of children that should be always available.
; pm.max_requests: The number of requests each child process should execute before re.
; pm.process_idle_timeout: The number of seconds after which an idle process will be killed.

pm.max_children = 50     ; Adjust based on RAM and expected load
pm.start_servers = 5     ; Initial number of workers
pm.min_spare_servers = 2 ; Minimum idle workers
pm.max_spare_servers = 10; Maximum idle workers
pm.max_requests = 500    ; Recycle processes after this many requests

; If pm = static, this is the value to use:
; pm.max_children = 50 ; Fixed number of children

; If pm = ondemand, this is the value to use:
; pm.max_children = 50
; pm.min_spare_servers = 1
; pm.max_spare_servers = 1
; pm.process_idle_timeout = 10s ; Idle processes are killed after 10 seconds

; Listen socket.
; Use a TCP socket for remote connections, or a Unix socket for local connections.
; For Nginx, a Unix socket is generally preferred for performance.
listen = /run/php/phpX.Y-fpm.sock ; Ensure Nginx user has read/write access

; If you prefer TCP:
; listen = 127.0.0.1:9000

; Set permissions for the socket
; listen.owner = www-data
; listen.group = www-data
; listen.mode = 0660

; Set user and group for the pool
user = www-data
group = www-data

; Set the default timezone
; date.timezone = UTC

; Other useful settings:
; request_terminate_timeout = 0 ; Set to a value (e.g., 60s) to limit script execution time
; request_slowlog_timeout = 10s ; Log scripts that take longer than this
; slowlog = /var/log/php/php-fpm-slow.log

; Error reporting and logging
; error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; log_level = notice ; Available levels: debug, notice, warn, error, critical
; access.log = /var/log/php/php-fpm.access.log
; access.format = "%R - %u %{%s}e \"%{REQUEST_METHOD}x %U%q%{%HTTP_PROTOCOL}x\" %s %O \"%{HTTP_REFERER}x\" \"%{HTTP_USER_AGENT}x\""

; Security settings
; cgi.fix_pathinfo = 0 ; Recommended for security

After modifying the configuration, reload PHP-FPM:

sudo systemctl reload phpX.Y-fpm

Redis Performance Tuning

Redis is an in-memory data structure store, often used as a cache, message broker, and database. Optimizing Redis involves tuning its memory usage, persistence, and network configuration.

Memory Management

The `maxmemory` directive is crucial to prevent Redis from consuming all available RAM. Setting `maxmemory-policy` determines how Redis evicts keys when `maxmemory` is reached.

`redis.conf` Tuning

# redis.conf

# Set the maximum memory Redis can use.
# Example: 2GB. Adjust based on your EC2 instance's RAM and other processes.
maxmemory 2gb
# maxmemory 0 # No limit (not recommended in production)

# Eviction policy when maxmemory is reached.
# volatile-lru: Remove LRU keys among those with an expire set.
# allkeys-lru: Remove LRU keys among all keys.
# volatile-random: Remove random keys among those with an expire set.
# allkeys-random: Remove random keys among all keys.
# volatile-ttl: Remove keys with the shortest TTL among those with an expire set.
# noeviction: Return errors on write operations when memory limit is reached. (Default)
maxmemory-policy allkeys-lru # Recommended for caching scenarios

# Save configuration (RDB snapshots)
# save 900 1    # Save if 1 key changed in 900 seconds
# save 300 10   # Save if 10 keys changed in 300 seconds
# save 60 10000 # Save if 10000 keys changed in 60 seconds
# For caching-heavy workloads, you might disable or reduce saving frequency
# to avoid I/O overhead, especially if data loss is acceptable.
save "" # Disable RDB persistence if data loss is acceptable

# Append Only File (AOF) for durability.
# If you disable RDB, AOF can provide better durability.
# appendonly no # Set to 'yes' if you need durability and have disabled RDB
# appendfilename "appendonly.aof"
# appendfsync everysec # 'always' is too slow, 'no' is faster but less safe

# Network configuration
# bind 127.0.0.1 ::1 # Bind to localhost only for security if accessed only locally
# bind 0.0.0.0 # Bind to all interfaces (use with caution and strong passwords/firewalls)
port 6379

# Security
# requirepass your_strong_password # Set a strong password

# Client configuration
# maxclients 10000 # Maximum number of concurrent clients

# TCP keepalive
tcp-keepalive 300 # Send TCP ACK to clients to keep connections alive

After modifying `redis.conf`, restart the Redis service:

sudo systemctl restart redis-server

Monitoring and Diagnostics

Continuous monitoring is key to identifying bottlenecks and validating tuning efforts. Use tools like:

  • Nginx: `stub_status` module, `error.log`, `access.log`, `X-Cache-Status` header.
  • Gunicorn: `status` command, logs, systemd service status.
  • PHP-FPM: `status` page (if enabled), logs, systemd service status.
  • Redis: `redis-cli INFO` command (especially `memory`, `stats`, `clients`), `redis-cli MONITOR`.
  • System Metrics: `top`, `htop`, `vmstat`, `iostat`, CloudWatch metrics (CPU Utilization, Network In/Out, Disk I/O, Memory).

Example Redis `INFO` output snippet:

# redis-cli INFO memory
used_memory:2147483648
used_memory_human:2.00G
used_memory_rss:2200000000
used_memory_rss_human:2.05G
used_memory_peak:2147483648
used_memory_peak_human:2.00G
mem_fragmentation_ratio:1.02
# ... other sections like stats, clients, persistence ...

# redis-cli INFO stats
total_commands_processed:123456789
instantaneous_ops_per_sec:1500
keyspace_hits:100000000
keyspace_misses:23456789

Analyze these metrics to understand memory usage, hit/miss ratios for caches, command throughput, and identify potential resource contention. Adjust `maxmemory`, `maxmemory-policy`, worker counts, and timeouts based on observed behavior.

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

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala