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.
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.
sudo nginx -t
sudo nginx -T | less
Confirm:
- the expected server block is loaded
- the expected
includefiles are present - static file
locationblocks are not missing
2. Set worker capacity in the main config
Edit /etc/nginx/nginx.conf:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
Notes:
worker_processes auto;matches CPU countworker_connectionsaffects concurrency capacityuse epoll;is appropriate on Linuxworker_rlimit_nofilemust align with OS and systemd limits
3. Tune core HTTP behavior
Inside the http block, add or confirm:
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:
sendfileimproves static file transfer efficiencytcp_nopushandtcp_nodelayimprove packet behavior- shorter
keepalive_timeoutreduces idle socket usage keepalive_requestsallows connection reuse without holding sockets forever
4. Add file metadata caching
Inside the http block:
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:
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:
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:
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:
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:
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:
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.
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.
sudo nginx -t
sudo systemctl reload nginx
Do not reload if nginx -t fails.
12. Benchmark before and after
Test headers, compression, and concurrency.
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/andlocation /media/blocks. - Too few worker connections → concurrency stalls under burst traffic → raise
worker_connectionsand 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_timeoutand 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:
sudo nginx -T | less
Validate syntax before reload:
sudo nginx -t
Inspect service state:
systemctl status nginx --no-pager
Review recent Nginx errors:
sudo journalctl -u nginx -n 100 --no-pager
Tail logs:
sudo tail -f /var/log/nginx/error.log /var/log/nginx/access.log
Inspect active connections:
ss -tanp | grep ':80\|:443'
ss -tanp | grep nginx
Test response headers and compression:
curl -I -H 'Accept-Encoding: gzip' https://your-domain
Verify static files are served by Nginx:
curl -I https://your-domain/static/app.css
Benchmark locally:
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:
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
Add upstream timing to access logs if you need request timing detail:
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_timewith 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 -tpasses with no syntax errors. -
worker_processes autois enabled or intentionally set. -
worker_connectionsmatches expected concurrency and host limits. - Static and media files are served directly by Nginx.
- Gzip is enabled for text-based responses.
-
open_file_cacheis 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.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Production Checklist (Everything You Must Do)
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
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.