# 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.

Source: https://howhttpworks.com/headers/retry-after
Last reviewed: 2026-10-05

> **TL;DR:** `Retry-After` tells a client how long to wait before trying again, either as a number of seconds (`Retry-After: 120`) or as an HTTP date. Servers send it with `429 Too Many Requests` when a client hits a rate limit and with `503 Service Unavailable` during maintenance or overload. Well-behaved clients wait at least that long before retrying.

## What is Retry-After?

**Retry-After** is the server saying "try again in this long." Think of it as a "back soon" sign with the exact time written on it. You'll mostly see it on rate-limited APIs, during maintenance windows, and when a server is temporarily overloaded.

It tells clients when to come back, so they don't hammer a struggling server.

## How It Works

A server that's temporarily unavailable or rate-limiting requests responds like this:

```http
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: text/plain

Server is temporarily overloaded. Please try again in 2 minutes.
```

The client should wait 120 seconds before sending another request.

## Value Formats

### Seconds (Delay)

The number of seconds to wait:

```http
Retry-After: 60
```

Wait 60 seconds, then retry.

### HTTP Date

A specific date and time to retry after:

```http
Retry-After: Wed, 15 Jan 2025 16:00:00 GMT
```

Hold off until that moment.

## Common Use Cases

### Rate Limiting (429 Too Many Requests)

The client has called the API too often:

```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)

A planned maintenance window with a known end time:

```http
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>
```

### Server Overload (503 Service Unavailable)

A short-lived capacity problem:

```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)

On a redirect, Retry-After asks the client to wait before following the new location. The spec allows it, but you'll rarely see it in practice:

```http
HTTP/1.1 302 Found
Location: https://example.com/new-location
Retry-After: 5
```

## Rate Limiting Patterns

### Fixed Window

The counter resets at set 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

The limit is based on recent request history:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 300
X-RateLimit-Limit: 100
X-RateLimit-Window: 3600
```

### Token Bucket

Tokens refill at a steady rate:

```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

This version gives up after three retries, so a server that keeps answering 429 can't trap the client in a loop:

```javascript
async function fetchWithRetry(url, options = {}, retries = 3) {
  try {
    const response = await fetch(url, options)

    if ((response.status === 429 || response.status === 503) && retries > 0) {
      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, retries - 1)
      }
    }

    return response
  } catch (error) {
    throw error
  }
}
```

### Exponential Backoff

Pair the header with exponential backoff, so clients still spread out their retries when the header is missing:

```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 seconds = Number(retryAfter)
        const dateDelay = new Date(retryAfter) - Date.now()
        const baseDelay = retryAfter && Number.isFinite(seconds)
          ? seconds * 1000 // delay-seconds form
          : dateDelay > 0 ? dateDelay : 1000 // HTTP-date form, else 1s
        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

```javascript
const rateLimit = require('express-rate-limit')

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 100, // express-rate-limit v7+: `limit` (formerly `max`)
  standardHeaders: 'draft-7', // emits RateLimit and RateLimit-Policy
  legacyHeaders: false,
  handler: (req, res) => {
    const retryAfter = Math.max(1, Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000)) // resetTime is a Date
    res.set('Retry-After', String(retryAfter))
    res.status(429).json({
      error: 'Too Many Requests',
      retryAfter
    })
  }
})

app.use('/api', limiter)
```

### 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()
})
```

Once `maintenanceEnd` passes, the value goes negative, so turn maintenance mode off on time.

## Getting Retry-After right

**1. Send it with every 429 and 503:**

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```

**2. Pick realistic wait times:**

```text
Rate limiting: 60-3600 seconds
Server overload: 10-60 seconds
Maintenance: Actual maintenance duration
```

**3. Choose the format that fits the situation:**

```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. Explain the limit in the body too:**

```json
{
  "error": "Rate limit exceeded",
  "retryAfter": 3600,
  "limit": 100,
  "window": "1 hour"
}
```

**5. Add jitter so clients don't all retry at once:**

```javascript
const jitter = Math.random() * 0.1 // 0-10% extra, never earlier than asked
const delay = retryAfter * (1 + jitter)
```

## Common Mistakes

### Not Including Retry-After

Without the header, the client has to guess:

```http
❌ 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}
```

### Unrealistic Retry Times

```http
❌ Retry-After: 86400  (24 hours is too long)
✅ Retry-After: 3600   (1 hour is reasonable)
```

### Inconsistent Format

```http
❌ Sometimes seconds, sometimes minutes
✅ Always use seconds or always use HTTP dates
```

## Implementing Exponential Backoff with Retry-After

When a client gets a 429 or 503 with `Retry-After`, it should wait at least the time the server asked for. The catch: if thousands of clients all wake up the moment that period ends, they hit the server together and knock it over again. That's the thundering herd problem. Adding a little random jitter on top of the delay (up to 10% extra) spreads those retries out.

When there's no `Retry-After` header, exponential backoff is the usual fallback. Wait 1 second before the first retry, 2 before the second, 4 before the third, and so on. Cap the delay at something like 30 to 60 seconds, and set a maximum retry count so clients eventually give up.

## Related Headers

- [X-RateLimit-\*](https://howhttpworks.com/headers/x-ratelimit) - Additional rate limiting information
- [Cache-Control](https://howhttpworks.com/headers/cache-control) - Caching directives
- [Location](https://howhttpworks.com/headers/location) - Used with redirect status codes
- [Date](https://howhttpworks.com/headers/date) - Current server time for date calculations
