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

Vengala Vinay

Having 12+ Years of Experience in Software Development

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

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

Optimizing Nginx for High-Traffic Web Applications

Nginx is a cornerstone of modern web infrastructure, particularly for serving static assets and acting as a reverse proxy. Tuning Nginx effectively is crucial for handling high traffic volumes and minimizing latency. We’ll focus on key directives that impact performance and scalability, assuming a typical AWS EC2 instance setup.

Worker Processes and Connections

The worker_processes directive controls how many worker processes Nginx will spawn. A common recommendation is to set this to the number of CPU cores available on your server. This allows Nginx to fully utilize your hardware for handling concurrent requests. The worker_connections directive, on the other hand, defines the maximum number of simultaneous connections that each worker process can handle. The total number of connections is limited by the system’s file descriptor limit.

To determine the number of CPU cores, you can use the nproc command:

nproc

A typical Nginx configuration snippet for these directives would look like this:

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

events {
    worker_connections 1024; # Adjust based on system limits and expected load
    multi_accept on;
}

Keepalive Connections and Buffers

keepalive_timeout controls how long an idle HTTP connection will remain open. A shorter timeout can free up resources faster, while a longer timeout can improve performance for clients making multiple requests by reusing the same connection. client_header_buffer_size and large_client_header_buffers are important for handling request headers. If headers are too large for the buffer, Nginx will read them into a temporary file, which incurs I/O overhead.

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000; # Number of requests per keepalive connection

    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k; # Number of buffers and size

    # ... other http configurations
}

Gzip Compression and Caching

Enabling Gzip compression significantly reduces the size of responses sent to the client, saving bandwidth and improving load times. Browser caching can be leveraged by setting appropriate Expires or Cache-Control headers for static assets. Nginx’s built-in caching mechanisms can also be configured to cache responses from upstream servers.

http {
    # ...

    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;

    # Example for static asset caching
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public";
    }

    # ...
}

Tuning Gunicorn for Python WSGI Applications

Gunicorn (Green Unicorn) is a popular Python WSGI HTTP Server. Its performance is heavily influenced by the number of worker processes and the type of worker class used. For CPU-bound workloads, the sync worker class is common, while for I/O-bound workloads, gevent or eventlet workers can offer better concurrency.

Worker Processes and Threads

The --workers flag determines the number of worker processes. A common starting point is (2 * number_of_cores) + 1. For the sync worker class, each worker handles one request at a time. If you are using threaded workers (e.g., with gevent or eventlet), you can also configure the number of threads per worker using the --threads flag.

Example Gunicorn command line for a Python application:

gunicorn --workers 3 --bind 0.0.0.0:8000 myapp.wsgi:application

For a more robust setup, consider using a configuration file:

# gunicorn_config.py
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
threads = 2 # If using threaded workers
worker_class = "sync" # or "gevent", "eventlet"
loglevel = "info"
accesslog = "-"
errorlog = "-"

And then run Gunicorn with:

gunicorn -c gunicorn_config.py myapp.wsgi:application

Timeouts and Buffers

--timeout specifies the number of seconds Gunicorn will wait for a worker to process a request before timing out. This should be set higher than your longest expected request processing time. --keep-alive controls the number of seconds a worker will stay alive after sending a response.

# gunicorn_config.py
# ...
timeout = 120 # seconds
keepalive = 5 # seconds
# ...

Tuning PHP-FPM for PHP Applications

PHP-FPM (FastCGI Process Manager) is the standard way to run PHP applications in production, especially when paired with Nginx. Its performance hinges on the process management settings, which dictate how PHP processes are spawned and managed.

Process Manager Settings

PHP-FPM offers three primary process management strategies: static, dynamic, and ondemand. Each has its trade-offs:

  • static: A fixed number of child processes are always kept running. This offers the most predictable performance but can be wasteful if traffic is highly variable.
  • dynamic: The number of child processes varies between a specified minimum and maximum based on demand. This is a good balance for most workloads.
  • ondemand: Processes are spawned only when requests arrive and are killed after a period of inactivity. This saves resources but can introduce latency for the first few requests after an idle period.

The configuration file for PHP-FPM (typically /etc/php/X.Y/fpm/pool.d/www.conf, where X.Y is your PHP version) contains directives like:

; Choose one of: static, dynamic, ondemand
pm = dynamic

; For pm = dynamic:
pm.max_children = 100       ; Maximum number of children that can be started.
pm.min_spare_servers = 10   ; Minimum number of servers that should be kept running.
pm.max_spare_servers = 50   ; Maximum number of servers that should be kept spare.

; For pm = static:
; pm.max_children = 50      ; Number of child processes to always keep active.

; For pm = ondemand:
; pm.max_children = 50      ; Maximum number of children that can be started.
; pm.min_spare_servers = 1  ; Minimum number of servers that should be kept running.
; pm.max_spare_servers = 3  ; Maximum number of servers that should be kept spare.
; pm.process_idle_timeout = 10s ; The timeout for killing a previously active process.

; Other important settings
request_terminate_timeout = 30s ; Timeout for script execution
; listen = /run/php/phpX.Y-fpm.sock ; Socket for Nginx to connect to
; listen.owner = www-data
; listen.group = www-data
; listen.mode = 0660

Tuning pm.max_children is critical. Setting it too high can exhaust server memory, while setting it too low can lead to request queuing and slow response times. A common approach is to monitor your server’s memory usage and adjust this value accordingly. For dynamic, ensure min_spare_servers and max_spare_servers are set to reasonable values to handle traffic spikes without excessive process creation overhead.

Tuning PostgreSQL on AWS RDS/EC2

PostgreSQL performance tuning is a deep subject, but for AWS deployments, we can focus on key parameters that significantly impact performance, particularly shared_buffers, work_mem, and connection pooling.

Shared Buffers

shared_buffers is arguably the most important parameter. It determines the amount of memory dedicated to PostgreSQL for caching data pages. A common recommendation is to set it to 25% of your total system RAM. For larger instances, this percentage might be reduced to avoid excessive swapping.

On AWS RDS, you can adjust this via the instance’s parameter group. On an EC2 instance, you would modify the postgresql.conf file.

# postgresql.conf
shared_buffers = 1024MB  # Example: 1GB for a 4GB RAM instance
# For larger instances, consider:
# shared_buffers = 25% of total RAM, but not more than ~8GB for optimal performance

Work Memory

work_mem controls the amount of memory that can be used for internal sort operations and hash tables before writing to temporary disk files. This is crucial for the performance of complex queries involving sorts, joins, and aggregations. Setting this too low can lead to slow queries, while setting it too high can exhaust memory if many queries require large sorts concurrently.

The total memory used by work_mem across all concurrent operations can be substantial. A good starting point is often 1-4% of total RAM per operation, but this requires careful monitoring. It’s often set at the session level for specific heavy queries.

# postgresql.conf
work_mem = 16MB # Default, adjust based on query analysis

# Example for a specific session:
# SET SESSION work_mem = '64MB';

Connection Pooling

Opening and closing database connections is an expensive operation. For applications with frequent, short-lived connections, a connection pooler like PgBouncer or built-in RDS Proxy can dramatically improve performance and reduce server load. PgBouncer can be configured to use different pooling modes: Session, Transaction, and Statement.

A typical pgbouncer.ini configuration:

[pgbouncer]
; Listen address and port
listen_addr = 0.0.0.0
listen_port = 6432

; Pooler settings
pool_mode = transaction ; Transaction pooling is generally recommended
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5

; Database connection string (replace with your actual details)
[databases]
mydb = host=your-rds-endpoint.rds.amazonaws.com port=5432 dbname=your_db user=your_user password=your_password

Ensure your application connects to PgBouncer’s port (e.g., 6432) instead of directly to PostgreSQL.

WAL Tuning

For write-heavy workloads, tuning Write-Ahead Logging (WAL) parameters can be beneficial. wal_buffers controls the amount of memory used for WAL data before writing to disk. max_wal_size (PostgreSQL 9.5+) and min_wal_size (PostgreSQL 9.5+) control the total size of WAL files, influencing checkpoint frequency.

# postgresql.conf
wal_buffers = 16MB # Default is usually -1 (auto-tuned based on shared_buffers)
max_wal_size = 4GB # Adjust based on write volume and recovery time objectives
min_wal_size = 1GB # Ensure enough space for checkpoints
checkpoint_completion_target = 0.9 # Spread checkpoints over 90% of the interval

Putting It All Together: AWS Deployment Considerations

When deploying this stack on AWS, consider the following:

  • Instance Sizing: Choose EC2 instance types that provide a good balance of CPU, RAM, and network I/O for your workload. For RDS, select instance classes that match your performance needs.
  • EBS Volumes: For EC2 deployments, use appropriate EBS volume types (e.g., gp3 for general purpose, io2 for high I/O) and provision sufficient IOPS if needed.
  • Security Groups: Configure security groups to allow traffic only from necessary sources (e.g., Nginx from the internet, application server from Nginx, database from application server).
  • Monitoring: Utilize AWS CloudWatch for monitoring CPU utilization, memory usage, disk I/O, network traffic, and database performance metrics. Set up alarms for critical thresholds.
  • Load Balancing: Employ AWS Elastic Load Balancing (ELB) to distribute traffic across multiple Nginx instances for high availability and scalability.
  • Auto Scaling: Configure Auto Scaling Groups for EC2 instances to automatically adjust the number of Nginx or application servers based on demand.

This playbook provides a solid foundation for optimizing your Nginx, Gunicorn/FPM, and PostgreSQL stack on AWS. Remember that continuous monitoring and iterative tuning based on real-world performance data are key to maintaining optimal performance.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability
  • Scala Pekko vs. Go Goroutines: Actor Model vs. CSP for Event-Driven Reactive Systems
  • Java Loom Virtual Threads vs. Go Goroutines: Under-the-Hood Scheduler and Thread Overhead Comparison

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (584)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (806)
  • PHP (5)
  • PHP Development (21)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (19)
  • Ruby on Rails (1)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (7)
  • WordPress Theme Development (357)

Recent Posts

  • Go Goroutines vs. Node.js Event Loop: Scaling I/O-Bound Microservices Under High Load
  • Elixir Phoenix vs. Go Gin: Concurrency Models and Fault Tolerance Under Peak Request Volume
  • Python Celery vs. Go Channels: Distributed Task Queue Overhead and Memory Reliability

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (806)
  • Debugging & Troubleshooting (584)
  • Security & Compliance (543)
  • SEO & Growth (491)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala