Reference Guide Detailed deployment notes with production context and concrete examples.

Flask Nginx Performance Tuning Guide

If you're trying to improve Flask production performance behind Nginx, this guide shows you how to tune the reverse proxy step-by-step. It focuses on the Nginx settings that most directly affect throughput, keepalive behavior, buffering, compression, and static file delivery.

Quick Fix / Quick Setup

Use this baseline if you need a fast starting point before deeper tuning.

bash
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sudo tee /etc/nginx/conf.d/flask-performance.conf > /dev/null <<'EOF'
worker_processes auto;

# put inside http context if your distro uses includes under /etc/nginx/conf.d/
worker_rlimit_nofile 65535;

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

sudo awk '1; /http \{/ && !x {print "    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 15;\n    keepalive_requests 1000;\n    types_hash_max_size 2048;\n    client_max_body_size 20M;\n    server_tokens off;\n    open_file_cache max=10000 inactive=30s;\n    open_file_cache_valid 60s;\n    open_file_cache_min_uses 2;\n    open_file_cache_errors on;\n    gzip on;\n    gzip_comp_level 5;\n    gzip_min_length 1024;\n    gzip_vary on;\n    gzip_proxied any;\n    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/xml image/svg+xml;"; x=1 }' /etc/nginx/nginx.conf | sudo tee /etc/nginx/nginx.conf.new > /dev/null
sudo mv /etc/nginx/nginx.conf.new /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl reload nginx

Use this as a baseline only. Confirm your distro layout before editing nginx.conf. Keep upstream proxy settings and static file locations in the site config.

What’s Happening

Nginx performance issues usually come from inefficient worker settings, poor keepalive behavior, missing compression, excessive disk I/O, or sending static files through Gunicorn instead of Nginx.

For Flask apps, Nginx should terminate client connections, cache file metadata, compress text assets, and proxy dynamic requests efficiently to Gunicorn.

The goal is to reduce request overhead at the edge so Gunicorn handles only application work.

Step-by-Step Guide

1. Validate the current Nginx configuration

Check syntax and print the full active configuration.

bash
sudo nginx -t
sudo nginx -T | less

Confirm:

  • the expected server block is loaded
  • the expected include files are present
  • static file location blocks are not missing

2. Set worker capacity in the main config

Edit /etc/nginx/nginx.conf:

nginx
worker_processes auto;
worker_rlimit_nofile 65535;

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

Notes:

  • worker_processes auto; matches CPU count
  • worker_connections affects concurrency capacity
  • use epoll; is appropriate on Linux
  • worker_rlimit_nofile must align with OS and systemd limits

3. Tune core HTTP behavior

Inside the http block, add or confirm:

nginx
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 15;
    keepalive_requests 1000;
    types_hash_max_size 2048;
    client_max_body_size 20M;
    server_tokens off;
}

Why:

  • sendfile improves static file transfer efficiency
  • tcp_nopush and tcp_nodelay improve packet behavior
  • shorter keepalive_timeout reduces idle socket usage
  • keepalive_requests allows connection reuse without holding sockets forever

4. Add file metadata caching

Inside the http block:

nginx
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

This reduces repeated file lookup overhead for static-heavy deployments.

5. Enable gzip compression for text responses

Inside the http block:

nginx
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types
    text/plain
    text/css
    text/xml
    application/json
    application/javascript
    application/xml+rss
    application/xml
    image/svg+xml;

This is usually worth enabling for:

  • HTML
  • CSS
  • JavaScript
  • JSON APIs
  • XML
  • SVG

6. Serve static files directly from Nginx

Do not send static files through Gunicorn.

Example server block:

nginx
server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/current/app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        log_not_found off;
    }

    location /media/ {
        alias /srv/myapp/shared/media/;
        expires 7d;
        access_log off;
        log_not_found off;
    }
}

Use immutable only when filenames are content-hashed.

If static assets are not loading correctly, see Flask Static Files Not Loading in Production.

7. Define an upstream for Gunicorn TCP deployments

If Gunicorn listens on TCP, define an upstream:

nginx
upstream flask_app {
    server 127.0.0.1:8000;
    keepalive 32;
}

If using a Unix socket, skip upstream keepalive and use the socket path directly.

8. Tune proxy behavior for Flask requests

Inside the dynamic request location block:

nginx
location / {
    proxy_pass http://flask_app;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    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_connect_timeout 5s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;

    proxy_buffering on;
    proxy_buffers 16 16k;
    proxy_buffer_size 16k;
}

If using a Unix socket:

nginx
location / {
    proxy_pass http://unix:/run/gunicorn.sock;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    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_connect_timeout 5s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;

    proxy_buffering on;
    proxy_buffers 16 16k;
    proxy_buffer_size 16k;
}

9. Use a full server block baseline

Example complete baseline:

nginx
upstream flask_app {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/current/app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        log_not_found off;
    }

    location / {
        proxy_pass http://flask_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        proxy_buffering on;
        proxy_buffers 16 16k;
        proxy_buffer_size 16k;
    }
}

10. Check system limits

If you increase Nginx capacity, confirm the process is allowed enough open files.

bash
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'

If needed, adjust:

  • systemd LimitNOFILE
  • /etc/security/limits.conf
  • host kernel limits

11. Reload safely

Validate syntax before reload.

bash
sudo nginx -t
sudo systemctl reload nginx

Do not reload if nginx -t fails.

12. Benchmark before and after

Test headers, compression, and concurrency.

bash
curl -I -H 'Accept-Encoding: gzip' https://your-domain
curl -I https://your-domain/static/app.css
wrk -t4 -c100 -d30s http://127.0.0.1/
ab -n 1000 -c 50 http://127.0.0.1/

Compare:

  • latency
  • requests per second
  • upstream timing
  • error rate
  • static asset response behavior

13. Check whether the bottleneck is really Nginx

If Nginx is tuned and requests are still slow, the problem is often upstream:

  • Gunicorn worker count too low
  • blocking Flask code
  • slow database queries
  • external API latency

If that applies, continue with Flask Gunicorn Performance Tuning Guide and verify your deployment baseline in Flask Production Checklist (Everything You Must Do).

Common Causes

  • Static files routed through Gunicorn instead of Nginx → wastes app workers on file delivery → add dedicated location /static/ and location /media/ blocks.
  • Too few worker connections → concurrency stalls under burst traffic → raise worker_connections and confirm OS file descriptor limits.
  • No gzip or poor compression config → larger responses and slower transfer → enable gzip for text-based content.
  • Long keepalive timeout with low worker capacity → idle clients consume sockets → reduce keepalive_timeout and raise worker capacity appropriately.
  • Proxy buffering disabled without a reason → increased upstream pressure and slower response handling → enable proxy_buffering on; for standard web traffic.
  • Access logging for every static request → unnecessary disk I/O → disable access logs for static paths or move logs to faster storage.
  • Timeout values too low → valid requests fail under load → increase proxy read/send timeouts only as needed.
  • Timeout values too high → slow upstreams tie up Nginx resources longer than needed → set practical limits and fix the app or Gunicorn if requests are slow.
  • File metadata cache not enabled → repeated stat calls on many assets → configure open_file_cache.
  • Underlying Gunicorn bottleneck → Nginx appears slow but upstream is saturated → tune Gunicorn workers, threads, or app code.

Debugging Section

Check effective config:

bash
sudo nginx -T | less

Validate syntax before reload:

bash
sudo nginx -t

Inspect service state:

bash
systemctl status nginx --no-pager

Review recent Nginx errors:

bash
sudo journalctl -u nginx -n 100 --no-pager

Tail logs:

bash
sudo tail -f /var/log/nginx/error.log /var/log/nginx/access.log

Inspect active connections:

bash
ss -tanp | grep ':80\|:443'
ss -tanp | grep nginx

Test response headers and compression:

bash
curl -I -H 'Accept-Encoding: gzip' https://your-domain

Verify static files are served by Nginx:

bash
curl -I https://your-domain/static/app.css

Benchmark locally:

bash
wrk -t4 -c100 -d30s http://127.0.0.1/
ab -n 1000 -c 50 http://127.0.0.1/

Check open file limits for Nginx workers:

bash
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'

Add upstream timing to access logs if you need request timing detail:

nginx
log_format timing '$remote_addr - $host "$request" '
                  'status=$status request_time=$request_time '
                  'upstream_time=$upstream_response_time';

access_log /var/log/nginx/access.log timing;

What to look for:

  • repeated upstream timed out
  • frequent too many open files
  • static file requests hitting upstream instead of local alias paths
  • high $upstream_response_time with low Nginx request overhead
  • compression not applied to large text responses

If you see 502 errors while tuning, use Fix Flask 502 Bad Gateway (Step-by-Step Guide).

Checklist

  • nginx -t passes with no syntax errors.
  • worker_processes auto is enabled or intentionally set.
  • worker_connections matches expected concurrency and host limits.
  • Static and media files are served directly by Nginx.
  • Gzip is enabled for text-based responses.
  • open_file_cache is configured for static-heavy apps.
  • Proxy headers to Gunicorn are set correctly.
  • Keepalive and timeout values are explicitly defined.
  • Nginx reload completes without downtime.
  • Load test results improve or remain stable after changes.
  • Error logs show no upstream or buffering regressions.

FAQ

Q: Should I use a Unix socket or TCP between Nginx and Gunicorn?
A: Use a Unix socket for a single-host setup unless TCP fits your deployment model better. The larger performance gains usually come from correct worker, buffering, and static file settings.

Q: Should proxy_buffering be on for Flask?
A: Yes for most standard web requests. Disable it only for streaming or long-lived response patterns that require immediate flushing.

Q: Does gzip help API responses?
A: Yes. JSON responses often compress well and reduce bandwidth and transfer time.

Q: Will increasing worker_connections fix application slowness?
A: No. It increases concurrency capacity but does not make slow Flask code or database queries faster.

Q: Should I cache dynamic Flask pages in Nginx?
A: Only if your response behavior is safe to cache. Start with static asset caching first.

Final Takeaway

Tune Nginx to do edge work efficiently: manage connections well, compress text responses, serve static files directly, and keep proxy behavior clean. If performance remains poor after that, the next bottleneck is usually Gunicorn or the Flask app itself.