Your application was working fine yesterday. Today? nginx 400 bad request everywhere. No obvious changes. No deployment. Just errors.
Welcome to the club.
What 400 Actually Means (And Doesn't)
The server understood the request syntax but refuses to process it. That's the official line. In practice, nginx throws 400 for dozens of reasons, and most error messages tell you absolutely nothing useful.
"Bad Request" could mean malformed headers, oversized cookies, invalid characters in the URL, timeout issues, proxy misconfigurations... the list goes on. Without proper logging, you're guessing.
Case Study: The Friday Night Disaster
A video streaming platform pushed a minor frontend update Friday at 6 PM. By 6:47 PM, nginx 400 errors spiked from 0.2% to 34% of requests. Revenue dropped $4,800 per hour.
The culprit? Their new analytics library sent custom headers with unencoded special characters. Specifically, pipe symbols (|
) in user agent strings. Nginx rejected every request with these headers.
Timeline:
- 6:00 PM: Deploy frontend v2.4.1
- 6:47 PM: Error rate spikes, alerts fire
- 7:23 PM: Team identifies pattern in logs
- 7:41 PM: Rollback initiated
- 8:15 PM: Traffic normalized
Total cost: $9,600 in lost revenue, 3 hours of engineering time, damaged user trust.
The fix: URL-encode all custom headers before transmission. Testing with production-like data would have caught it.
Turn On Debug Logging First
Stop whatever you're doing and enable info-level logging. Nginx logs the actual reason for 400 errors at the info level, not at the default error level.
Edit your nginx.conf:
error_log /var/log/nginx/error.log info;
Restart nginx. Trigger the error again. Now check the logs:
tail -f /var/log/nginx/error.log
You'll see specific messages like "client intended to send too large body" or "client sent invalid method while reading client request line." Actual clues instead of generic 400s.
This one change solves more problems than any other troubleshooting step. Error logs are typically located at /var/log/nginx/error.log, and examining entries related to 400 errors helps identify specific issues.
Common error log patterns and meanings:
Log Message | Meaning | Fix |
---|---|---|
"client intended to send too large body" | Body exceeds limit | Increase client_max_body_size |
"client sent invalid method" | Unrecognized HTTP verb | Check limit_except directives |
"client sent invalid request" | Malformed URL/headers | URL-encode special characters |
"SSL_do_handshake() failed" | TLS mismatch | Check SSL configuration |
"upstream prematurely closed" | Backend died | Investigate backend health |
Invalid Request Method Problems
Sometimes clients send HTTP requests nginx does not recognize. Sounds weird until you realize how many broken clients exist.
Check your logs for "client sent invalid method." Common causes:
- HTTP/2 clients hitting HTTP/1.1-only backends
- Browsers sending OPTIONS preflight requests nginx rejects
- Load balancers using health check methods nginx blocks
- API clients making up their own HTTP verbs
Fix the method whitelist:
location / {
limit_except GET POST PUT DELETE PATCH HEAD OPTIONS {
deny all;
}
}
Or if you're seeing legitimate requests rejected, verify your proxy configuration. When using nginx as a reverse proxy, ensure Host, X-Real-IP, and X-Forwarded-For headers are correctly set to avoid backend server issues.
Advanced method handling:
# Allow custom methods for specific APIs
location /api/v2/ {
limit_except GET POST PUT DELETE PATCH HEAD OPTIONS TRACE {
deny all;
}
}
# Block all methods except GET for static content
location /static/ {
limit_except GET {
deny all;
}
}
URL Length and Special Characters
Browsers allow ridiculously long URLs. Nginx does not, by default.
The large_client_header_buffers
directive limits request line length. Default allows around 8KB. Seems generous until someone pastes a 16KB base64-encoded parameter into a GET request.
Increase the limit:
http {
large_client_header_buffers 4 32k;
}
Special characters cause different problems. Spaces, pipes, brackets – anything not properly URL-encoded makes nginx angry. Check your logs for "client sent invalid request."
Common culprits:
- Search queries with unencoded spaces (convert to
%20
) - File paths with brackets or quotes
- Query parameters with special characters
- Emojis in URLs (yes, really)
Modern frameworks usually handle encoding. Legacy systems? Not so much.
URL encoding reference for common issues:
Space → %20
Pipe | → %7C
Bracket [ → %5B
Bracket ] → %5D
Quote " → %22
Ampersand & → %26 (in query strings)
Proxy Protocol Mismatches
Running nginx behind a load balancer adds another failure point. Nginx returns HTTP 400 errors when PROXY protocol is misconfigured between HAProxy and nginx.
If your load balancer sends PROXY protocol headers but nginx is not configured to accept them, every request fails with 400.
Enable PROXY protocol support:
server {
listen 80 proxy_protocol;
set_real_ip_from 10.0.0.0/8;
real_ip_header proxy_protocol;
}
The set_real_ip_from
line tells nginx which IPs are trusted proxies. Without this, nginx treats proxy headers as malicious spoofing attempts.
AWS ALB, Google Cloud Load Balancer, and Cloudflare all handle this differently. Check your infrastructure documentation because getting this wrong breaks everything.
Cloud provider proxy configurations:
# AWS ALB (no PROXY protocol, uses X-Forwarded-For)
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
# Google Cloud Load Balancer (similar to AWS)
set_real_ip_from 10.128.0.0/9;
real_ip_header X-Forwarded-For;
# Cloudflare (provides IP ranges list)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
real_ip_header CF-Connecting-IP;
WebSocket Connection Failures
WebSocket upgrades fail with 400 when nginx is not configured properly. The client sends an upgrade request, nginx rejects it, connection dies.
Enable WebSocket support:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
The proxy_http_version 1.1
line is critical. WebSockets require HTTP/1.1 or newer. Nginx defaults to HTTP/1.0 for proxying, which breaks upgrades.
Missing the Upgrade
and Connection
headers? Nginx treats the request as invalid and returns 400.
If you're building real-time features with a mobile app developer houston team, WebSocket configuration becomes critical. Chat apps, live updates, gaming – all need this working correctly.
WebSocket troubleshooting checklist:
- [ ] proxy_http_version set to 1.1
- [ ] Upgrade header passed through
- [ ] Connection header set to "upgrade"
- [ ] proxy_read_timeout increased (prevents premature closes)
- [ ] Backend actually supports WebSockets
- [ ] Firewall allows persistent connections
Body Size Limits
Nginx limits request body size to 1MB by default. Upload anything larger, get 400 errors.
Increase the limit:
client_max_body_size 50M;
But here's what the documentation skips: this applies to the entire request body, not individual files. Multipart form uploads with multiple files add up fast.
Also, this limit exists per location block. You can set different limits for different endpoints:
location /api/upload/ {
client_max_body_size 100M;
client_body_timeout 300s;
}
location /api/data/ {
client_max_body_size 5M;
client_body_timeout 60s;
}
File uploads need higher limits. JSON payloads should stay small. Tune accordingly.
Real-world body size recommendations:
Endpoint Type | Recommended Limit | Timeout |
---|---|---|
Image upload | 10-25MB | 120s |
Video upload | 500MB-2GB | 600s |
JSON API | 1-5MB | 30s |
CSV import | 50-100MB | 180s |
PDF documents | 25-50MB | 90s |
Request Timeout Issues
Client takes too long to send data? 400 error. The client taking too long to send the request body can trigger 400 errors in nginx.
The client_body_timeout
directive controls this:
client_body_timeout 60s;
Default is 60 seconds. Usually enough unless you're dealing with slow connections or massive uploads.
The client_header_timeout
matters too:
client_header_timeout 10s;
If a client takes more than 10 seconds to send headers, something's wrong. Either the connection is terrible or it's a bot probing your server.
Timeout tuning by use case:
# Mobile apps on cellular networks
client_body_timeout 120s;
client_header_timeout 15s;
# Internal APIs on fast networks
client_body_timeout 30s;
client_header_timeout 5s;
# Public file upload service
client_body_timeout 300s;
client_header_timeout 10s;
Worker Connection Exhaustion
Nginx reaching the maximum number of worker connections can cause 400 errors. Check your current limits:
events {
worker_connections 1024;
}
Each nginx worker process can handle that many concurrent connections. With four workers, that's 4096 total. Sounds like plenty until you're serving thousands of requests per second.
Monitor actual usage:
nginx -V 2>&1 | grep -o 'worker_connections [0-9]*'
Increase if needed:
events {
worker_connections 4096;
}
But also check worker_rlimit_nofile
. It limits open file descriptors per worker:
worker_rlimit_nofile 10000;
Connections and file descriptors must align. Otherwise nginx hits OS limits and starts rejecting requests.
Production sizing calculation:
Expected concurrent users: 10,000
Avg connections per user: 2 (HTTP/2 multiplexing)
Total connections needed: 20,000
Worker processes (CPU cores): 8
Connections per worker: 20,000 / 8 = 2,500
Set worker_connections: 4096 (leave headroom)
Set worker_rlimit_nofile: 8192 (2x connections)
Broken Module Configurations
A required nginx module not being loaded can trigger 400 errors. This happens after nginx updates or when moving configurations between servers.
Check loaded modules:
nginx -V
Look at the --with-*
flags. If your config references modules not compiled in, requests fail.
Common issues:
- GeoIP module missing after upgrade
- Security modules not installed
- Custom third-party modules unavailable
Either install the missing module or remove references from your configuration. Do not leave broken module directives hanging around.
Module verification script:
#!/bin/bash
# Check if required modules are available
nginx -V 2>&1 | grep -q "http_geoip_module" || echo "MISSING: GeoIP"
nginx -V 2>&1 | grep -q "http_realip_module" || echo "MISSING: RealIP"
nginx -V 2>&1 | grep -q "http_ssl_module" || echo "MISSING: SSL"
nginx -V 2>&1 | grep -q "stream_module" || echo "MISSING: Stream"
SSL/TLS Handshake Problems
HTTPS requests failing with 400 often indicate SSL misconfiguration. Check certificates first:
openssl s_client -connect yourdomain.com:443
Look for certificate chain issues, expired certs, or protocol mismatches.
Mixed HTTP/HTTPS configurations cause problems too:
# WRONG - Do not do this
server {
listen 80;
listen 443 ssl;
# This breaks everything
}
# CORRECT - Separate server blocks
server {
listen 80;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
Separate your HTTP and HTTPS server blocks. Do not mix them unless you really know what you're doing.
TLS configuration best practices (2025):
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
Cache and Cookie Corruption
Corrupted cookies or outdated cached data cause conflicts that trigger 400 errors. Clear browser cookies and cache, test again.
If the error disappears, your application is setting malformed cookies. Check your session management code.
Server-side caching can cause similar issues. Fastcgi_cache, proxy_cache – if they cache malformed responses, clients keep seeing 400 errors even after you fix the underlying issue.
Clear nginx cache:
rm -rf /var/cache/nginx/*
systemctl reload nginx
Cache debugging configuration:
# Add cache status to response headers
add_header X-Cache-Status $upstream_cache_status;
# Log cache hits/misses
log_format cache '$remote_addr - $upstream_cache_status [$time_local] '
'"$request" $status $body_bytes_sent';
access_log /var/log/nginx/cache.log cache;
Systematic Debugging Approach
When facing repeated 400 errors, follow this sequence:
- Enable info-level logging - See actual error reasons
- Read the actual error message - Do not guess
- Test with curl - Eliminate browser issues
- Check proxy headers - If behind load balancer
- Verify buffer sizes - Match traffic patterns
- Review recent changes - Configuration drift
- Test from different networks - Rule out firewall rules
Most problems come from configuration drift. Something changed somewhere. Find what changed, revert it, test again.
Do not randomly tweak settings hoping something sticks. That's how you end up with configuration files nobody understands and production servers held together with prayers.
curl debugging commands:
# Basic request with verbose output
curl -v https://example.com
# Test specific HTTP method
curl -X POST -v https://example.com/api
# Send custom headers
curl -H "X-Custom: value" -v https://example.com
# Test with large cookie
curl -b "session=$(printf 'x%.0s' {1..8000})" -v https://example.com
# Follow redirects
curl -L -v https://example.com
# Test WebSocket upgrade
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
https://example.com/ws
Production Monitoring and Alerting
Set up proper monitoring before 400 errors become a crisis. Track these metrics:
Key metrics to monitor:
- 400 error rate (errors per second, percentage of total requests)
- Error patterns by endpoint
- Geographic distribution of errors
- User agent analysis (mobile vs desktop vs bot)
- Time-based patterns (specific hours/days)
Alerting thresholds:
# Example Datadog monitor
name: "High 400 Error Rate"
query: "avg(last_5m):sum:nginx.requests{status:400}.as_rate() > 50"
message: |
400 error rate exceeded 50/second
Check nginx error logs: tail -f /var/log/nginx/error.log
Recent deploys: {{#is_alert}}@slack-ops{{/is_alert}}
Configure alerts that fire before users notice problems. A 5% error rate for 5 minutes should wake someone up.
Cost of 400 Errors
Every 400 error has business impact:
Revenue calculation:
- Average order value: $75
- Conversion rate: 2%
- 400 error rate: 5%
- Daily visitors: 50,000
Lost conversions = 50,000 × 0.05 × 0.02 = 50 orders/day
Revenue loss = 50 × $75 = $3,750/day
Monthly impact = $112,500
Even small error rates compound fast. A misconfigured nginx server costs real money every hour it runs.
Engineering cost:
- Incident detection: 30 minutes
- Root cause analysis: 2 hours
- Fix and deploy: 1 hour
- Total: 3.5 hours × $150/hour = $525 per incident
Prevention beats firefighting. Proper configuration and monitoring cost less than repeated incidents.
Pre-Deployment Checklist
Test these before pushing nginx changes to production:
- [ ] Syntax check:
nginx -t
- [ ] Config validation:
nginx -T | grep error
- [ ] Buffer sizes match expected traffic
- [ ] Timeout values appropriate for use case
- [ ] SSL certificates valid and not expiring soon
- [ ] Proxy protocol configuration matches load balancer
- [ ] Worker connections sized for peak load
- [ ] Module dependencies available
- [ ] Logging level appropriate (info for troubleshooting)
- [ ] Cache directories exist and writable
- [ ] Test with curl before allowing real traffic
- [ ] Monitoring alerts configured
Staging environment testing:
# Load test with realistic headers
ab -n 10000 -c 100 -H "Cookie: session=test123" \
https://staging.example.com/
# Test edge cases
curl -X POST -d @large-payload.json staging.example.com/api
curl -H "X-Custom-Header: $(printf 'x%.0s' {1..1000})" staging.example.com
Top comments (0)