//------------------------------------------------------------------- //-------------------------------------------------------------------
Nginx Slow Requests

Nginx Slow Requests: Why CPU Looks Fine but Requests Are Slow

Nginx can feel slow even when CPU usage is only 10–20%, and everything else looks normal. Users may see timeouts, slower page loads, and worse p95 latency. This happens because CPU isn’t the only limit; Nginx slow requests often come from disk I/O wait, hitting connection limits, network bottlenecks, or a slow upstream and backend service.

This guide shows you how to find and fix slow requests in Nginx. You will learn to measure latency with p95, separate Nginx delays from upstream delays, check OS limits, review TLS cost, spot disk or network bottlenecks, apply safe tuning changes, and confirm results with load testing.

For high-traffic sites and strict-SLA APIs, upgrading to Dedicated Servers can deliver more stable performance and full server control.

PerLod Hosting provides a low-latency and high-throughput base where Nginx tuning has the most impact.

Nginx Slow Requests Signs

It is important to know the signs of slow requests in Nginx. Here is the most common slow request symptoms checklist:

1. Low CPU utilization with high latency: CPU stays under 30%, but requests still take too long, which means the server looks idle, yet users wait seconds. This usually means Nginx is waiting for disk I/O, the network, or available connections instead of doing CPU work.

2. Discrepancy between total and upstream response times: If logs show $request_time is much higher than $upstream_response_time, Nginx is adding extra delay.

For example, request_time=1.2s but upstream_response_time=0.2s means about 1 second is spent inside Nginx, not in your backend app.

3. Increasing p95 and p99 latency: Average response time may look fine like 200ms, but p95 and p99 can still be high like 500ms and above. This means a noticeable group of users is seeing much slower responses. p95 is the time under which 95% of requests finish, so the slowest 5% take longer than that.

4. Connection limit exhaustion: If stub_status shows active connections close to your worker_connections limit, Nginx is running out of connection capacity. When it hits the limit, new connections may sit in the listen backlog or get rejected. Even before the limit is reached, being near saturation can create queues, which show up as Nginx slow requests.

5. Too many open files errors: he error log contains messages like Too many open files, which means Nginx has hit its open-file limit, so workers can’t accept new connections or open more files. Every client connection, upstream connection, log file, and even a static file being served uses a file descriptor.

6. Random timeout errors: Clients may see 504 Gateway Timeout errors, especially during traffic spikes. This usually means Nginx couldn’t finish the request before the timeout, often because the upstream is unreachable, too busy, or requests are building up in a queue.

7. High I/O wait percentage: If top shows wa (I/O wait) staying above 1%, the CPU isn’t actually busy; it’s waiting for disk reads and writes to finish.

When disk I/O is slow, Nginx workers get stuck on file operations and can’t handle other requests, so requests become slow even though the CPU looks fine.

8. Slow client signs: Users with fast internet may say the site loads fine at first, but later requests or big downloads become slow, even though bandwidth is available.

This often points to buffering problems; either Nginx is waiting too long for the upstream to produce data before it can start sending, or slow clients are forcing Nginx to keep upstream connections open for longer than needed.

Now that you have understood the symptoms of slow requests in Nginx, proceed to the following step to measure the slow requests.

Measure Slow Requests in Nginx: p95 and upstream time

As you must know, if you don’t track the right metrics, it’s hard to find the real cause or know if your changes actually helped. In this step, we want to measure the p95 and upstream time in Nginx.

Configure Nginx access Logging

Nginx’s default access logs usually don’t include the timing details you need to troubleshoot slow requests.

You can create a custom log format that logs both the full request time and upstream time so you can see whether the delay is in Nginx or in the upstream app:

http {
    log_format performance '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          'rt=$request_time uct=$upstream_connect_time '
                          'uht=$upstream_header_time urt=$upstream_response_time';
    
    access_log /var/log/nginx/access.log performance;
}

Explanation:

  • $request_time: Total time for the whole request, from first byte received to last byte sent.
  • $upstream_connect_time: Time to connect to the upstream.
  • $upstream_header_time: Time until Nginx gets the first response byte.
  • $upstream_response_time: Total time Nginx spends getting the full upstream response.

Note: If $request_time is much bigger than $upstream_response_time, the delay is mostly in Nginx, like TLS, connections, buffering, or sending data to slow clients. If the two numbers are close, the backend or app is the main reason requests are slow.

Enable Nginx stub_status for Live Monitoring

The ngx_http_stub_status_module provides instant visibility into active connections, handled requests, and connection states. To enable it, you can use:

server {
    listen 80;
    
    location /nginx_status {
        stub_status on;
        access_log off;
        allow 127.0.0.1;
        deny all;
    }
}

After reloading Nginx, you can check the status with:

curl http://localhost/nginx_status

In your output, you must see something similar to this:

Active connections: 291
server   accepts  handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106

Explanation of the status metrics:

  • Active connections: All open client connections, including idle ones. If this is near the worker_connections limit, you’re close to the limit.
  • accepts and handled: Accepted connections vs actually handled connections. If they differ, Nginx is hitting a limit, like open files or connections.
  • requests: Total HTTP requests served. If requests per connection are low, keep-alive may not be working well.
  • Reading: Connections where Nginx is reading the request. High values can mean slow clients and the network.
  • Writing: Connections where Nginx is sending the response. High values can mean slow clients, big responses, or limited bandwidth.
  • Waiting: Idle keepalive connections waiting for the next request.

Calculate p95 Latency from Access Logs

Average response time gives a general idea, but p95 shows real user experience better. p95 is the time that 95% of requests finish under, so only the slowest 5% take longer.

You can use awk to calculate p95 from request_time:

# Extract request times, sort numerically, calculate 95th percentile
awk '{print $NF}' /var/log/nginx/access.log | \
grep -v '-' | \
sort -n | \
awk 'BEGIN {c=0} {a[c++]=$1} END {print a[int(c*0.95)]}'

For upstream response time p95, you can use:

# Extract upstream_response_time field specifically
awk '{print $(NF-2)}' /var/log/nginx/access.log | \
grep -oP 'urt=\K[0-9.]+' | \
sort -n | \
awk 'BEGIN {c=0} {a[c++]=$1} END {print a[int(c*0.95)]}'

Note: For production environments, you can integrate Nginx with Prometheus and Grafana for real-time tracking. Install nginx-prometheus-exporter, configure Prometheus to scrape the exporter endpoint, and query p95 in PromQL:

histogram_quantile(0.95, 
  sum(rate(nginx_http_request_duration_seconds_bucket[5m])) by (le)
)

This shows the p95 request time for the last 5 minutes. You can put it on a Grafana chart to spot latency spikes, see trends over time, and confirm whether your tuning actually improved performance.

Find Nginx Bottlenecks from Upstream Bottlenecks

The relationship between $request_time and $upstream_response_time tells you where the slowness is coming from.

Here are three scenarios:

1: They’re close, for example, request_time is 0.850 and upstream_response_time is 0.820.

The near values show that the backend or app is slow, and Nginx is mostly just passing traffic through. Focus on the app, database, external APIs, or adding more upstream capacity.

2: The $request_time is much bigger, for example, request_time is 1.200, and upstream_response_time is 0.180.

It means Nginx is adding a delay. Common reasons are TLS handshakes, opening new backend connections, buffers spilling to disk, slow clients, or OS limits like open files or connections.

3: $upstream_response_time is 0 or ““. Nginx didn’t get a real response from the upstream, like backend unreachable, network issue, upstream timeout, or a proxy_pass problem.

Nginx Slow Requests Root Causes

Knowing exactly what is slowing things down helps you fix the real problem instead of randomly changing settings. With Nginx slow requests, the causes usually fall into six main groups, and each one has its own symptoms and way to diagnose it.

OS Limits: File Descriptors and Connections

Nginx needs file descriptors to handle connections and files. One client connection uses at least one, and when Nginx is proxying, it may use another one for the upstream connection too. Logs and static files also use file descriptors, and if Nginx runs out, it can’t accept new connections, so requests get slow or start failing.

Many Linux systems limit each process to about 1024 open files by default, which often means only around 512 concurrent connections in practice.

If you don’t raise these limits, Nginx can’t use the server’s full power, even when CPU, RAM, and network still have plenty of room.

There are three file descriptor limits you need to align:

  • System limit (fs.file-max): Total file descriptors for the whole server.
  • Service and user limit (ulimit -n / systemd): Max file descriptors allowed for the Nginx service or user.
  • Nginx limit (worker_rlimit_nofile): Max file descriptors Nginx allows per worker.

All three must be set correctly. If one is lower, for example, systemd is 1024, it will override the higher values, and Nginx will still hit the smaller limit.

For checking the current limits, you can use the commands below:

# System-wide maximum
cat /proc/sys/fs/file-max

# Current soft limit for Nginx workers
cat /proc/$(pgrep -u www-data nginx | head -n 1)/limits | grep "open files"

# Alternative: check ulimit for your user
ulimit -Hn  # Hard limit
ulimit -Sn  # Soft limit

The worker_connections is the max connections per Nginx worker. Theoretical max clients is:

max_clients = worker_processes × worker_connections

If Nginx is proxying, each request may use two connections, so the real capacity is closer to:

max_proxied_clients ≈ (worker_processes × worker_connections) / 2

Also, Nginx needs extra file descriptors for logs and files, so a safe rule is:

worker_rlimit_nofile ≥ worker_connections × 2 + extra

For example, if worker_connections is 4096, set worker_rlimit_nofile to around 10000.

TLS Overhead: Handshakes and Session Resumption

TLS keeps traffic secure, but it can add noticeable delay. A full TLS handshake takes extra round-trips between the client and server, which costs time and some CPU.

A complete TLS 1.2 handshake requires two round-trips (2-RTT) between the client and the server before real data can start. For a user about 50ms away, that can add around 200ms of delay just to set up HTTPS. If lots of requests open new connections, this extra delay adds up fast.

TLS 1.3 reduces the handshake to one round-trip (1-RTT), which cuts latency in half. TLS 1.3 is also faster because it removes older, unused crypto options, so there’s less work to do during setup.

If performance matters, enable TLS 1.3 in Nginx while keeping TLS 1.2 for compatibility:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;  # TLS 1.3 doesn't use this

TLS session resumption lets returning clients reuse an existing TLS session, so they don’t need a full handshake every time. This can cut the TLS handshake cost heavily.

You can enable it in Nginx like this:

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets on;

The “shared:SSL:10m” means Nginx keeps a 10MB shared cache for TLS sessions that all worker processes can use. As a rough sizing rule, ~1MB can store about 4,000 sessions, so 10MB is about 40,000 sessions.

Even with session resumption, TLS still takes noticeable CPU, and a full handshake is far more expensive than a resumed one. Using variable certificates can also make each handshake slower.

For very busy setups, it can help to terminate TLS on a separate front proxy, load balancer, or use hardware and TLS acceleration so your main Nginx layer spends less time on crypto.

Nginx Disk Bottlenecks: I/O wait and Buffer to Disk

Modern servers can have plenty of CPU and RAM, but disk I/O can still be the slow part, especially if Nginx has to write data to temporary files.

By default, Nginx buffers requests and responses in memory, but it starts writing to disk when:

  • The client request body is bigger than client_body_buffer_size.
  • The upstream response is bigger than the available proxy_buffers.
  • Logging is heavy, and log writes can’t keep up; for example, logs aren’t buffered enough.

Because a disk is much slower than RAM, even one slow disk write can pause an Nginx worker for a while, which makes requests feel slow even when the CPU looks idle.

You can use atop and iotop for real-time I/O tracking.

Common I/O bottleneck causes include:

  • Excessive logging
  • Buffering to disk
  • Cache storage
  • Slow disk hardware

Mitigation strategies:

Enable access log buffering to batch disk writes:

access_log /var/log/nginx/access.log main buffer=64k flush=5s;

Increase buffer sizes to keep more data in memory:

client_body_buffer_size 128k;
proxy_buffers 8 16k;
proxy_buffer_size 16k;

Use faster storage and disable proxy buffering for fast clients.

Nginx Network Bottlenecks: Bandwidth and Saturation

Network problems look different from CPU or disk issues. Requests may begin fast but get slow during big downloads, users in some regions may see higher delay, and performance can drop during traffic spikes, even when the server still has free CPU and RAM.

Your server may have a 1Gbps network interface, but actual throughput depends on multiple factors:

  • Upstream bandwidth
  • Peering arrangements
  • Geographic distance
  • TCP window scaling

When clients are slow, Nginx may keep upstream connections busy while it slowly sends data to the user. Turning proxy_buffering on lets Nginx read the response from the upstream fast, free that upstream connection, then send the buffered response to the slow client:

location / {
    proxy_pass http://backend;
    proxy_buffering on;
    proxy_buffers 8 16k;
    proxy_buffer_size 16k;
}

Without keepalive, every request has to open a new TCP connection, which adds extra delay. With keepalive, the client reuses the same connection, so requests are faster:

http {
    keepalive_timeout 65;
    keepalive_requests 100;
}

This keeps connections open for 65 seconds or up to 100 requests, which is especially helpful for APIs where clients send many requests in a short time.

Also, creating new TCP connections from Nginx to your upstream adds delay, especially over long distances or with TLS. Reuse upstream connections with upstream keepalive:

upstream backend {
    server 192.168.1.10:8080;
    keepalive 32;  # Maintain up to 32 idle keepalive connections
    keepalive_timeout 60s;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Nginx Upstream Delays: Latency and Connection Pools

Slow backends will make requests slow, even if Nginx is fine. To tell where the delay is, look at the upstream timing fields:

  • $upstream_connect_time: Time to connect to the backend.
  • $upstream_header_time: Time until the first byte comes back (backend processing).
  • $upstream_response_time: Total time spent getting the full backend response.

If $upstream_connect_time is high, it usually means network or queueing problems reaching the backend or limits on the backend side.

If $upstream_header_time is high, the backend is slow, like app code, database, or external API calls.

If backends can only handle a limited number of connections, extra requests will queue, and $upstream_connect_time will spike.

To fix it, spread traffic across multiple backends and cap connections per server:

upstream backend {
    least_conn;  # Route to server with fewest active connections
    server backend1.example.com:8080 max_conns=100;
    server backend2.example.com:8080 max_conns=100;
    server backend3.example.com:8080 max_conns=100;
    
    keepalive 64;
    keepalive_timeout 60s;
}

If you want controlled waiting during spikes, you can use a queue:

upstream backend {
    server backend1.example.com:8080 max_conns=50;
    queue 100 timeout=30s;
}

Proxy Buffering Misconfiguration in Nginx

Proxy buffering speeds things up with slow clients, but bad buffer sizes can cause high memory use or force Nginx to write to disk.

With proxy_buffering on, Nginx reads the upstream response quickly into buffers, then sends it to the client. This frees the backend faster and prevents slow clients from holding upstream connections open.

Use proxy_buffering off only if:

  • Clients are definitely fast like an internal API.
  • You need real streaming.
  • You’re very tight on RAM.

Here is a basic example:

location / {
    proxy_pass http://backend;
    proxy_buffering on;
    
    # Headers buffer (usually small)
    proxy_buffer_size 4k;
    
    # Response body buffers (8 buffers of 4k each = 32k total)
    proxy_buffers 8 4k;
    
    # Maximum busy buffers that can transmit to client while receiving from upstream
    proxy_busy_buffers_size 8k;
    
    # If response exceeds buffers, write overflow to temporary file
    proxy_max_temp_file_size 1024m;
    proxy_temp_file_write_size 64k;
}

Simple rules to choose buffer sizes:

  • Pick sizes based on your usual response size.
  • If buffers are too small, Nginx may spill to disk.
  • If buffers are too big, RAM usage grows fast.

Warning: Buffer memory can grow fast:

total buffer RAM ≈ workers × connections per worker × buffer size per request.

For example, 4 workers × 1024 connections × 64 KB ≈ 256 MB used just for buffers, so plan your RAM carefully.

How To Fix Nginx Slow Requests?

At this point, you can apply fixes based on what you found, and change one thing at a time so you can measure what actually helped.

Raise the system-wide file limit:

sudo nano /etc/sysctl.conf

Add:

fs.file-max = 100000

Apply and check:

sudo sysctl -p
cat /proc/sys/fs/file-max

Raise user limits:

sudo nano /etc/security/limits.conf

Add with your Nginx user:

www-data soft nofile 65536
www-data hard nofile 65536

Raise the systemd limit for Nginx:

sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo nano /etc/systemd/system/nginx.service.d/override.conf

Add:

[Service]
LimitNOFILE=65536

Reload and verify:

sudo systemctl daemon-reload
sudo systemctl restart nginx
cat /proc/$(pgrep -u www-data nginx | head -n 1)/limits | grep "open files"

Update Nginx worker settings in /etc/nginx/nginx.conf:

user www-data;
worker_processes auto;  # Automatically detect CPU core count
worker_rlimit_nofile 65536;

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

Test configuration and reload:

sudo nginx -t
sudo systemctl reload nginx

You can enable Nginx upstream keepalive to avoid reconnecting to the backend for every request:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    
    # Maintain pool of 32 idle keepalive connections per worker
    keepalive 32;
    
    # Keep connections alive for 60 seconds
    keepalive_timeout 60s;
    
    # Maximum 1000 requests per keepalive connection
    keepalive_requests 1000;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://backend;
        
        # REQUIRED: Enable HTTP/1.1 for upstream
        proxy_http_version 1.1;
        
        # REQUIRED: Clear Connection header (default is "close")
        proxy_set_header Connection "";
        
        # Optional: Set appropriate timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Note: proxy_http_version 1.1 and clearing the Connection header are required for upstream keepalive to work.

Validate Nginx Slow Requests Fixes

It is recommended to measure before and after applying the changes, so you know your tuning really helped and didn’t break anything.

Before making any changes, capture baseline measurements:

# Record current configuration
sudo nginx -T > /tmp/nginx-baseline-config.txt

# Check stub_status
curl http://localhost/nginx_status | tee /tmp/baseline-stub-status.txt

# Capture p95 latency from recent logs
awk '{print $NF}' /var/log/nginx/access.log | \
grep -v '-' | sort -n | \
awk 'BEGIN {c=0} {a[c++]=$1} END {print "p95:", a[int(c*0.95)]; print "p99:", a[int(c*0.99)]}' \
> /tmp/baseline-percentiles.txt

# Run load test (adjust parameters for your environment)
wrk -t4 -c100 -d60s http://localhost/ > /tmp/baseline-wrk.txt

If you don’t have wrk, you can run:

ab -c 100 -n 10000 http://localhost/

If you change 5 things at once, you won’t know what helped. Do this loop:

  • Make one change
  • Test config: sudo nginx -t
  • Reload: sudo systemctl reload nginx
  • Wait 2–3 minutes
  • Run the same wrk test again
  • Compare p95, p99, Requests, and errors

Example workflow:

# Change 1: Increase worker connections
# Edit nginx.conf: worker_connections 2048 -> 4096
sudo nginx -t && sudo systemctl reload nginx
sleep 180  # Wait for connection pool to stabilize
wrk -t4 -c100 -d60s http://localhost/ > /tmp/test1-worker-connections.txt

# Change 2: Enable upstream keepalive
# Edit nginx.conf: Add keepalive 32 to upstream block
sudo nginx -t && sudo systemctl reload nginx
sleep 180
wrk -t4 -c100 -d60s http://localhost/ > /tmp/test2-upstream-keepalive.txt

# Change 3: Optimize gzip level
# Edit nginx.conf: gzip_comp_level 6 -> 4
sudo nginx -t && sudo systemctl reload nginx
sleep 180
wrk -t4 -c100 -d60s http://localhost/ > /tmp/test3-gzip-level.txt

After each change, re-check percentiles:

# Extract p95 and p99 from access logs after tuning
awk '{print $NF}' /var/log/nginx/access.log | \
grep -v '-' | sort -n | \
awk 'BEGIN {c=0} {a[c++]=$1} END {
    print "p50:", a[int(c*0.50)];
    print "p95:", a[int(c*0.95)];
    print "p99:", a[int(c*0.99)];
}'

If p95 drops a lot, like 20%+, that’s a real win. If p99 gets worse, you may have helped average users but hurt the slowest users.

Note: Keep the previous Nginx configuration available for instant rollback:

# Before changes
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup-$(date +%Y%m%d)

# If issues arise after deployment
sudo cp /etc/nginx/nginx.conf.backup-20260108 /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl reload nginx

FAQs

Why is my Nginx CPU low, but requests are slow?

CPU isn’t everything; Nginx can be slow while it waits on disk I/O, network, or connection limits, so CPU may stay low.

Do I need upstream keepalive for my Nginx proxy?

Yes, if Nginx is proxying to a backend, upstream keepalive can reduce latency because it reuses existing backend connections instead of creating a new TCP connection for every request.

How do I know if I should upgrade to TLS 1.3?

If most traffic is HTTPS, TLS 1.3 speeds up new connections because it uses a 1‑RTT handshake. TLS 1.2 typically needs 2‑RTT. Enable session resumption first, then allow TLS 1.3:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets on;

Conclusion

Slow Nginx requests with low CPU usage usually mean the bottleneck is somewhere else, so basic CPU graphs won’t show the real problem. Common causes include:

  • File descriptor or connection limits that block new connections.
  • Slow TLS handshakes are adding noticeable latency per request.
  • High I/O wait that makes CPUs sit idle while waiting on storage.
  • Network limits that cap performance even when the CPU is fine.
  • ​Slow upstream or poor proxy settings that delay responses.

You must measure p95 latency, check logs, and watch stub_status to find the real bottleneck. Then fix only what’s needed. Make changes one at a time, load test, and keep monitoring. This can cut latency a lot and keep Nginx fast even when the CPU is low.

We hope you enjoy this guide. Subscribe to our X and Facebook channels to get the latest updates and articles.

For further reading:

Advanced Nginx Caching Strategies for High-Load Servers

How To Detect Disk I/O Bottlenecks in Linux

Post Your Comment

PerLod delivers high-performance hosting with real-time support and unmatched reliability.

Contact us

Payment methods

payment gateway
Perlod Logo
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.