- Home
- HTTP Headers
- Retry-After
Header
Retry-After
Learn how the Retry-After header tells clients how long to wait before retrying a request. Understand its use with 503, 429, and 301 status codes.
TL;DR: Tells clients how long to wait before retrying a request. Essential for rate limiting (429) and server maintenance (503) responses.
What is Retry-After?
Retry-After tells clients “please wait this long before trying again.” It’s like a “come back later” sign that specifies exactly when “later” is. This header is crucial for rate limiting, maintenance windows, and handling temporary server overload.
It helps prevent clients from overwhelming servers while providing clear guidance on when to retry.
How It Works
When a server is temporarily unavailable or rate-limiting requests:
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: text/plain
Server is temporarily overloaded. Please try again in 2 minutes.
```text
The client should wait 120 seconds before making another request.
## Value Formats
### Seconds (Delay)
Number of seconds to wait:
```http
Retry-After: 60
Wait 60 seconds before retrying.
HTTP Date
Specific date/time when to retry:
Retry-After: Wed, 15 Jan 2025 16:00:00 GMT
```text
Don't retry until this exact time.
## Common Use Cases
### Rate Limiting (429 Too Many Requests)
API has been called too frequently:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Content-Type: application/json
{
"error": "Rate limit exceeded",
"message": "You can make 100 requests per hour. Try again in 1 hour."
}
Server Maintenance (503 Service Unavailable)
Planned maintenance window:
HTTP/1.1 503 Service Unavailable
Retry-After: Wed, 15 Jan 2025 18:00:00 GMT
Content-Type: text/html
<h1>Scheduled Maintenance</h1>
<p>We'll be back at 6 PM GMT.</p>
```text
### Server Overload (503 Service Unavailable)
Temporary capacity issues:
```http
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json
{
"error": "Server overloaded",
"message": "Please retry in 30 seconds"
}
Redirects (301/302 with delay)
Rarely used, but possible:
HTTP/1.1 302 Found
Location: https://example.com/new-location
Retry-After: 5
```text
## Rate Limiting Patterns
### Fixed Window
Reset at specific intervals:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 1800
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1642781234
Sliding Window
Based on request history:
HTTP/1.1 429 Too Many Requests
Retry-After: 300
X-RateLimit-Limit: 100
X-RateLimit-Window: 3600
```text
### Token Bucket
Refill rate-based:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Tokens: 0
X-RateLimit-Refill-Rate: 10
Client Implementation
JavaScript Fetch with Retry
async function fetchWithRetry(url, options = {}) {
try {
const response = await fetch(url, options)
if (response.status === 429 || response.status === 503) {
const retryAfter = response.headers.get('Retry-After')
if (retryAfter) {
const delay = isNaN(retryAfter)
? new Date(retryAfter) - new Date()
: parseInt(retryAfter) * 1000
console.log(`Rate limited. Waiting ${delay}ms...`)
await new Promise((resolve) => setTimeout(resolve, delay))
// Retry the request
return fetchWithRetry(url, options)
}
}
return response
} catch (error) {
throw error
}
}
```javascript
### Exponential Backoff
Combine with exponential backoff for robustness:
```javascript
async function fetchWithBackoff(url, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url)
if (response.ok) return response
if (response.status === 429 || response.status === 503) {
const retryAfter = response.headers.get('Retry-After')
const baseDelay = retryAfter ? parseInt(retryAfter) * 1000 : 1000
const delay = baseDelay * Math.pow(2, attempt) // Exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
throw new Error(`HTTP ${response.status}`)
} catch (error) {
if (attempt === maxRetries - 1) throw error
const delay = 1000 * Math.pow(2, attempt)
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
Server Implementation
Express.js Rate Limiting
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
headers: true, // Include rate limit headers
handler: (req, res) => {
const resetTime = new Date(Date.now() + 15 * 60 * 1000)
res.set('Retry-After', Math.ceil((resetTime - Date.now()) / 1000))
res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil((resetTime - Date.now()) / 1000)
})
}
})
app.use('/api', limiter)
```javascript
### Maintenance Mode
```javascript
app.use((req, res, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
const maintenanceEnd = new Date('2025-01-15T18:00:00Z')
const retryAfter = Math.ceil((maintenanceEnd - new Date()) / 1000)
res.set('Retry-After', retryAfter)
return res.status(503).json({
error: 'Service Unavailable',
message: 'Scheduled maintenance in progress',
retryAfter: retryAfter,
maintenanceEnd: maintenanceEnd.toISOString()
})
}
next()
})
Best Practices
1. Always include with 429 and 503 responses:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```text
**2. Use reasonable retry times:**
```text
Rate limiting: 60-3600 seconds
Server overload: 10-60 seconds
Maintenance: Actual maintenance duration
```text
**3. Be consistent with time format:**
```http
✅ Use seconds for short delays:
Retry-After: 60
✅ Use HTTP date for specific times:
Retry-After: Wed, 15 Jan 2025 16:00:00 GMT
4. Include helpful error messages:
{
"error": "Rate limit exceeded",
"retryAfter": 3600,
"limit": 100,
"window": "1 hour"
}
```javascript
**5. Implement jitter to prevent thundering herd:**
```javascript
const jitter = Math.random() * 0.1 // ±10%
const delay = retryAfter * (1 + jitter)
Common Mistakes
Not Including Retry-After
❌ HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{"error": "Rate limited"}
✅ HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{"error": "Rate limited", "retryAfter": 3600}
```text
### Unrealistic Retry Times
```http
❌ Retry-After: 86400 (24 hours is too long)
✅ Retry-After: 3600 (1 hour is reasonable)
Inconsistent Format
❌ Sometimes seconds, sometimes minutes
✅ Always use seconds or always use HTTP dates
Related Headers
- X-RateLimit-* - Additional rate limiting information
- Cache-Control - Caching directives
- Location - Used with redirect status codes
- Date - Current server time for date calculations
Implementing Exponential Backoff with Retry-After
When a client receives a 429 or 503 with Retry-After, it should respect the specified delay exactly. However, if many clients all retry at the same moment when the Retry-After period expires, they create a thundering herd that can overwhelm the server again immediately. Adding a small random jitter (±10% of the retry delay) spreads the retries over time and prevents synchronized retry storms.
For clients that do not receive a Retry-After header, exponential backoff is the standard fallback: wait 1 second before the first retry, 2 seconds before the second, 4 seconds before the third, and so on, up to a maximum delay. Cap the maximum delay at a reasonable value (30-60 seconds) to avoid clients waiting indefinitely. Always combine exponential backoff with a maximum retry count to prevent infinite retry loops.
Frequently Asked Questions
What is Retry-After?
Retry-After tells clients how long to wait before retrying a request. It is used with 503 Service Unavailable, 429 Too Many Requests, and 3xx redirects.
What format does Retry-After use?
Either seconds (Retry-After: 120) or an HTTP date (Retry-After: Wed, 21 Oct 2026 07:28:00 GMT). Seconds is simpler and more commonly used.
Should clients always respect Retry-After?
Yes, respecting Retry-After prevents overwhelming servers and improves recovery. Ignoring it may result in continued failures or being blocked.
How do I implement Retry-After in my API?
Send Retry-After with 429 responses indicating when the rate limit resets. Calculate seconds until reset or use the reset timestamp. Document this for API users.