Web Server Advanced

Troubleshooting Nginx 503 Errors: Rate Limit Exceeded on CentOS Stream / Rocky Linux

Resolve Nginx 503 Service Unavailable errors caused by rate limiting on CentOS Stream and Rocky Linux. Diagnose, adjust Nginx configs, and secure your web server.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Resolve Nginx 503 Service Unavailable errors caused by rate limiting on CentOS Stream and Rocky Linux. Diagnose, adjust Nginx configs, and secure your web server.

When your Nginx web server starts returning 503 Service Unavailable errors, especially under periods of high traffic or suspicious activity, it often indicates that a configured rate limit has been triggered. This guide delves into diagnosing and resolving "Nginx rate limit exceeded" issues on CentOS Stream and Rocky Linux distributions, ensuring your services remain available while protecting against abuse.

Symptom & Error Signature

Users attempting to access your Nginx-powered website or API will consistently receive a 503 Service Unavailable response. From a server perspective, you'll observe specific entries in your Nginx error logs, indicating that a rate limit has been reached.

Typical User Experience (Browser/curl):

curl -I https://yourdomain.com/
HTTP/1.1 503 Service Unavailable
Server: nginx
Date: Wed, 08 Nov 2023 10:30:00 GMT
Content-Type: text/html
Content-Length: 196
Connection: keep-alive
Retry-After: 5

Nginx Error Log (/var/log/nginx/error.log):

2023/11/08 10:30:00 [error] 12345#0: *123456 limiting requests, excess: 1.000 by zone "mylimit", client: 203.0.113.42, server: yourdomain.com, request: "GET /api/data HTTP/1.1", host: "yourdomain.com"
2023/11/08 10:30:01 [error] 12346#0: *123457 limiting requests, excess: 2.500 by zone "ip_burst_limit", client: 203.0.113.42, server: yourdomain.com, request: "POST /login HTTP/1.1", host: "yourdomain.com"

Nginx Access Log (/var/log/nginx/access.log):

203.0.113.42 - - [08/Nov/2023:10:30:00 +0000] "GET /api/data HTTP/1.1" 503 196 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
203.0.113.42 - - [08/Nov/2023:10:30:01 +0000] "POST /login HTTP/1.1" 503 196 "-" "python-requests/2.28.1"

Root Cause Analysis

The 503 Service Unavailable with a "limiting requests" error in Nginx logs directly points to Nginx's built-in rate limiting modules being triggered. Specifically, this indicates that the client's request rate has exceeded the thresholds defined by the limit_req_zone and limit_req directives, or that the number of concurrent connections has surpassed limit_conn_zone and limit_conn directives.

Here are the primary underlying reasons:

  1. Legitimate Traffic Spike: A sudden, legitimate surge in user traffic (e.g., viral content, marketing campaign success) can exceed existing, perhaps conservative, rate limits.
  2. Misconfigured Rate Limits: The configured rate limits might be too strict for your application's expected normal traffic patterns, inadvertently blocking valid users.
  3. Malicious Activity: This is a common defense mechanism against:
    • DDoS Attacks (Layer 7): Bots attempting to overwhelm the application layer with a flood of requests.
    • Brute-force Attacks: Repeated login attempts, API calls, or directory scanning.
    • Web Scraping: Bots excessively scraping content from your site.
  4. Backend/Upstream Issues (Indirect): While Nginx's rate limit is the direct cause of the 503, a struggling backend application or database might cause clients to retry requests more aggressively, inadvertently triggering Nginx's limits. However, Nginx's primary purpose here is to protect itself and the backend from being overloaded by too many requests, regardless of the backend's health.

Nginx's rate limiting is a powerful tool for maintaining stability, but it requires careful tuning to differentiate between legitimate high traffic and malicious floods.

Step-by-Step Resolution

Follow these steps to diagnose and resolve Nginx rate limit exceeded issues on your CentOS Stream or Rocky Linux server.

1. Identify the Source and Scope of the Problem

First, pinpoint which clients are hitting the rate limits and which specific limits are being triggered.

  1. Examine Nginx Error Logs: The Nginx error log (/var/log/nginx/error.log) is your primary source. Look for entries containing limiting requests, excess: as shown in the "Symptom & Error Signature" section. Pay attention to:

    • client: The IP address of the client triggering the limit.
    • zone: The name of the limit_req_zone that was exceeded.
    • request: The URI being requested.
    grep "limiting requests" /var/log/nginx/error.log | tail -n 50
    
  2. Analyze Access Logs for Specific IPs: Once you identify problematic client IPs from the error logs, check the access logs (/var/log/nginx/access.log) for the request patterns from those IPs. This helps determine if it's a legitimate user or a bot.

    # Replace 203.0.113.42 with the problematic IP
    grep "203.0.113.42" /var/log/nginx/access.log | less
    

    To identify top IPs hitting 503s due to rate limiting:

    awk '$9 == "503" {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 10
    

2. Review Current Nginx Rate Limiting Configuration

Locate your Nginx configuration files to understand the existing rate limit directives.

  1. Locate Nginx Configuration Files: The main Nginx configuration file is typically /etc/nginx/nginx.conf. Many setups use included files in /etc/nginx/conf.d/ or /etc/nginx/sites-enabled/ for virtual hosts.

    grep -r "limit_req_zone|limit_req|limit_conn_zone|limit_conn" /etc/nginx/
    
  2. Understand Key Directives:

    • limit_req_zone zone_name client_variable:size rate=rate [sync];

      • Defines a shared memory zone for storing request states.
      • client_variable: Usually $binary_remote_addr for unique client IPs.
      • size: Size of the zone (e.g., 10m for 10MB, which can store ~160,000 states).
      • rate: Max request rate (e.g., rate=1r/s for 1 request per second, rate=30r/m for 30 requests per minute).
    • limit_req zone=zone_name [burst=number] [nodelay | delay];

      • Applies the defined rate limit to a specific location, server, or http block.
      • zone=zone_name: References the shared memory zone.
      • burst=number: Allows requests to exceed the rate temporarily, buffering them up to number. Requests above burst are rejected.
      • nodelay: If burst is used, requests are served immediately without delay as long as they are within the burst limit. Without nodelay, requests exceeding the rate but within burst are delayed to match the rate. nodelay is often preferred for user experience.
    • limit_conn_zone client_variable zone=zone_name:size;

      • Defines a shared memory zone for storing concurrent connection states.
    • limit_conn zone=zone_name number;

      • Applies the connection limit. number is the maximum allowed concurrent connections per key.

    Example Nginx Configuration Snippet:

    # In http block (e.g., /etc/nginx/nginx.conf)
    http {
        limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
        limit_req_zone $binary_remote_addr zone=login_limit:5m rate=1r/s;
        limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
        server {
            listen 80;
            server_name yourdomain.com;
    
            # Apply a general request rate limit with burst and no delay
            location / {
                limit_req zone=mylimit burst=10 nodelay;
                limit_conn conn_limit 20; # Max 20 concurrent connections per IP
                # ... other configurations ...
            }
    
            # Stricter limit for a sensitive endpoint like login
            location /login {
                limit_req zone=login_limit burst=5 nodelay;
                # ... other configurations ...
            }
        }
    }
    

3. Adjusting Rate Limiting Parameters

Based on your analysis, choose the appropriate strategy. Always test Nginx configuration before reloading.

Blindly increasing rate limits can expose your server to DDoS attacks or resource exhaustion. Always analyze the traffic patterns before making significant changes. If you suspect an attack, consider blocking malicious IPs directly using a firewall first.

  1. Option A: Increase Limits (If Traffic is Legitimate) If your analysis shows that legitimate users or expected traffic spikes are triggering the limits, you might need to increase the rate or burst values.

    # Example: Increase general rate limit to 10 requests/second, allow burst of 20
    # Before: limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
    #         limit_req zone=mylimit burst=10 nodelay;
    # After (modify /etc/nginx/nginx.conf and/or server blocks):
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; # New rate
    # Then in your location block:
    # limit_req zone=mylimit burst=20 nodelay; # New burst
    
    • Increase rate: Allows more requests per unit of time.
    • Increase burst: Allows more requests to queue up without being immediately rejected. This is good for "bursty" legitimate traffic.
    • nodelay: Ensures that burst requests are processed immediately rather than being delayed. Use this if user experience is critical.
  2. Option B: Exempt Trusted IPs (Whitelisting) If specific internal tools, monitoring systems, or known partners are hitting the limits, you can whitelist their IP addresses. Use the geo module for this.

    # In http block (e.g., /etc/nginx/nginx.conf)
    http {
        geo $is_whitelisted {
            default 0;
            192.168.1.0/24 1; # Internal network
            203.0.113.10 1;   # Specific trusted IP
        }
    
        map $is_whitelisted $limit_zone_name {
            0 "mylimit";
            1 ""; # No limit zone for whitelisted IPs
        }
    
        # Apply limit_req based on the mapped zone name
        limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
        
        server {
            listen 80;
            server_name yourdomain.com;
    
            location / {
                # Only apply the limit if $limit_zone_name is not empty (i.e., not whitelisted)
                if ($limit_zone_name != "") {
                    limit_req zone=$limit_zone_name burst=10 nodelay;
                }
                # ...
            }
        }
    }
    
  3. Option C: Implement or Refine Advanced Limiting

    • Per-URI Limiting: Apply different limits to different paths. For example, a /login endpoint might need stricter limits than static asset requests. (Already shown in general example).
    • Combined Limits: You can combine multiple limit_req directives in a location block. Nginx will apply the strictest limit among them.
    • Limit by Header: Use $http_x_forwarded_for if behind a load balancer, but be wary of spoofed headers. real_ip_header and set_real_ip_from should be configured if you are behind a proxy.
    # Example for $http_x_forwarded_for (if correctly set by a trusted proxy)
    # Ensure you have 'real_ip_header X-Forwarded-For;' and 'set_real_ip_from trusted_ip_range;'
    # configured in your http block if relying on this.
    limit_req_zone $http_x_forwarded_for zone=proxy_limit:10m rate=5r/s;
    
  4. Option D: Implement Blocking/Firewalling (If Malicious Traffic) If you've identified specific IPs or patterns of malicious behavior, it's often better to block them at the firewall level (firewalld) or with tools like fail2ban rather than letting Nginx deal with every request.

    • Using firewalld (Recommended for CentOS/Rocky): To permanently block a suspicious IP:

      sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.42" drop'
      sudo firewall-cmd --reload
      
    • Using fail2ban: fail2ban can automatically monitor Nginx logs for rate limit errors and ban IPs using firewalld (or iptables).

      1. Install Fail2ban:
        sudo dnf install fail2ban -y
        
      2. Create a jail configuration for Nginx rate limits: Create /etc/fail2ban/jail.d/nginx-rate-limit.conf:
        [nginx-rate-limit]
        enabled = true
        port    = http,https
        filter  = nginx-rate-limit
        logpath = /var/log/nginx/error.log
        maxretry = 5
        findtime = 600
        bantime  = 86400 # Ban for 24 hours
        
      3. Create a filter for Nginx rate limit errors: Create /etc/fail2ban/filter.d/nginx-rate-limit.conf:
        [Definition]
        failregex = limiting requests, excess:.* by zone ".*", client: <HOST>,
        ignoreregex =
        
      4. Restart Fail2ban:
        sudo systemctl enable fail2ban
        sudo systemctl restart fail2ban
        
        fail2ban will now monitor your Nginx error logs and ban IPs that frequently trigger rate limit errors.

When modifying Nginx configuration, always test the syntax before reloading to avoid downtime.

sudo nginx -t

If the test is successful, you'll see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

4. Restart/Reload Nginx

After making any changes to Nginx configuration, you need to apply them.

sudo systemctl reload nginx

If you made changes to underlying modules or extremely critical paths, a full restart might be preferred, though reload is usually sufficient for configuration changes.

sudo systemctl restart nginx

5. Monitor and Iterate

After implementing changes, it's crucial to monitor your server's behavior and Nginx logs.

  1. Check Logs: Continuously monitor /var/log/nginx/error.log and /var/log/nginx/access.log to see if the 503 errors persist or if new rate limit errors appear.

  2. Server Metrics: Monitor server resource utilization (CPU, memory, network I/O) using tools like top, htop, nmon, or Grafana/Prometheus dashboards. Ensure that Nginx isn't just shifting the problem to another resource bottleneck.

  3. User Feedback: Gather feedback from users or check your application's error reporting to confirm the 503 Service Unavailable errors have ceased.

Rate limiting is an ongoing tuning process. You may need to adjust your limits multiple times to find the optimal balance between protection and user experience.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.