FastAPI performance optimization

Logo
Table of contents

Nginx in front of FastAPI

Another topic not strictly FastAPI performance optimization, but has a great benefit for the overall service if used. Nginx is a versatile service, web server, reverse proxy, WAF and so on. By using in front of the FastAPI application some functions can be decoupled from Gunicorn (request sanity check, timeout handling, backlogging, extra logging in case of error, app latency measurement and so on) Typical reverse proxy configuration uses TCP ports between the app and the web server / reverse proxy but it has negative impact on the concurrency as web server - app communication requires a pair of ports allocated for each connection. There are 65535 port all together, but some ranges are not for this purpose so the concurrency is limited, furthermore a port doesn’t become free immediately after a connection is closed The alternative solution which is mentioned in Uvicorn documentation suggests using sockets which indeed a better solution with some challenges.

FastAPI as non-root user

If you run your application as non-root user you need to be sure nginx user can read and write the socket. Fortunately Gunicorn supports umask. The most secure option is dedicating a group to this communication, making nginx’s and app’s user part of that group and limiting the communication to group read and write (umask 717)

Test environment

Measurements

CI run 29770319196 — Python 3.14, Ubuntu latest.

Runner configurations tested

The naming convention is Gunicorn w{workers}t{threads} where workers are pre-forked OS processes and threads are per-worker Python threads:

Gunicorn config Workers Threads What it means
w3t1 3 1 3 processes, 1 thread each — highest parallelism tested
w1t0 1 0 1 process, no threads — single-process baseline
w2t0 2 0 2 processes, no threads — default production config
w1t1 1 1 1 process, 1 thread — minimal threading
w2t1 2 1 2 processes, 1 thread each — balanced config
w1t2 1 2 1 process, 2 threads — thread-heavy single process
w2t2 2 2 2 processes, 2 threads each — max threading tested

Each configuration was tested with both TCP port and Unix socket communication, on different ports:

Config Port test port Socket test port
w3t1 8008 8009
w1t0 8140 8141
w2t0 8142 8143
w1t1 8144 8145
w2t1 8146 8147
w1t2 8148 8149
w2t2 8150 8151

Synchronous API endpoint with small request / response

Nginx - APP via port

Test attribute Test run 1 Test run 2 Test run 3 Average
Requests per second 1936.66 1868.64 1915.02 1906.77
Time per request [ms] 51.635 53.515 52.219 52.4563

Nginx - APP via socket

Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
Requests per second 7476.71 6645.0 7434.42 7185.38 +276.83%
Time per request [ms] 13.375 15.049 13.451 13.9583 38.5 ms

Observations

Asynchronous API endpoint with small request / response

Nginx - APP via port

Test attribute Test run 1 Test run 2 Test run 3 Average
Requests per second 2780.52 2764.32 2777.57 2774.14
Time per request [ms] 35.964 36.175 36.003 36.0473

Nginx - APP via socket

Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
Requests per second 7315.44 6159.01 7347.77 6940.74 +150.19%
Time per request [ms] 13.67 16.236 13.61 14.5053 21.54 ms

Observations

Synchronous API endpoint with 1MB response

Nginx - APP via port

Test attribute Test run 1 Test run 2 Test run 3 Average
Requests per second 2885.82 2758.47 2368.75 2671.01
Time per request [ms] 34.652 36.252 42.216 37.7067

Nginx - APP via socket

Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
Requests per second 6454.11 6795.05 6942.52 6730.56 +151.99%
Time per request [ms] 15.494 14.717 14.404 14.8717 22.83 ms

Observations

Asynchronous API endpoint with 1MB response

Nginx - APP via port

Test attribute Test run 1 Test run 2 Test run 3 Average
Requests per second 2713.03 2819.41 2761.47 2764.64
Time per request [ms] 36.859 35.468 36.213 36.18

Nginx - APP via socket

Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
Requests per second 6855.33 6831.25 7037.45 6908.01 +149.87%
Time per request [ms] 14.587 14.639 14.21 14.4787 21.7 ms

Observations

Conclusion

Apples to apples comparison

Scenario Port (rps) Socket (rps) Throughput gain Port latency (ms) Socket latency (ms) Latency gain
Sync, small response 1,907 7,185 +276.8% 52.5 14.0 73.3% lower
Async, small response 2,774 6,941 +150.2% 36.1 14.5 59.8% lower
Sync, 1MB response 2,671 6,731 +152.0% 37.7 14.9 60.5% lower
Async, 1MB response 2,765 6,908 +149.9% 36.2 14.5 59.9% lower

Two clear patterns emerge:

  1. Socket throughput is consistent across all scenarios: ~6,700-7,200 rps regardless of sync/async or payload size. The communication layer is not the bottleneck — the application is. Switching to sockets removes the networking overhead that was masking this.
  2. Port throughput is constrained by the TCP port pairing overhead: ranging from 1,907 to 2,774 rps. The sync+small case is hit hardest (+276%) because it is the most connection-intensive — each request opens and closes a TCP pair, while async reuse reduces the impact.

In short: Unix sockets deliver a flat ~7k rps regardless of endpoint type, while TCP ports cap at ~2-2.8k rps. The optimization is not scenario-dependent — it is a baseline improvement that applies universally.

Sample socket config is here

Impact

Quantified production impact

Assuming a single Gunicorn instance behind nginx:

Metric Before (port) After (socket) Improvement
Throughput (avg across scenarios) ~2,529 rps ~6,941 rps ~174% more requests/sec
Latency (avg across scenarios) ~40.6 ms ~14.5 ms ~64% lower latency
Effective concurrent users supported (p95 ~200ms budget) ~380 ~900 ~2.4x more headroom

When this matters most

Trade-offs to consider

Recommendation

Switch to Unix socket communication between nginx and Gunicorn by default. The throughput and latency gains are significant and consistent across all endpoint types. Only use TCP ports when you need cross-network communication between the reverse proxy and the application.

Pro tip:

user nobody nogroup;
pid /var/run/nginx.pid;
worker_processes 1;  # 1/CPU, to be configured, but Nginx is so powerful, 1 worker can easly handle 1-2k QPS 
events {
  worker_connections 4096; # increase in case of lot of clients
  accept_mutex off; # set to 'on' if nginx worker_processes > 1
  use epoll; # for Linux 2.6+
}

http {
  include mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  tcp_nodelay on; # avoid buffer
  access_log off;
  error_log stderr;
  upstream gunicorn {
    # fail_timeout=0 means we always retry an upstream even if it failed
    # to return a good HTTP response
    server unix:/tmp/gunicorn.sock fail_timeout=0;
    keepalive 8;
  }

  server {
    listen                                80;
    server_tokens                         off;
    client_max_body_size                  20M;

    gzip                                  on;
    gzip_proxied                          any;
    gzip_disable                          "msie6";
    gzip_comp_level                       6;
    gzip_min_length                       200; # check your average response size and configure accordingly

    location / {
      proxy_pass                          http://gunicorn;
      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  $http_x_forwarded_proto;
      proxy_pass_header                   Server;
      proxy_ignore_client_abort           on;
      proxy_connect_timeout               65s; #  65 here and 60 sec in gconf in order to time out at app side first
      proxy_read_timeout                  65s;
      proxy_send_timeout                  65s;
      proxy_redirect                      off;
      proxy_http_version                  1.1;
      proxy_set_header Connection         "";
      proxy_buffering                     off;
    }
    keepalive_requests                    5000;  
    keepalive_timeout                     120;
    set_real_ip_from                      10.0.0.0/8;
    set_real_ip_from                      172.16.0.0/12;
    set_real_ip_from                      192.168.0.0/16;
    real_ip_header                        X-Forwarded-For;
    real_ip_recursive                     on;
  }
}