How HTTP Works

Response header

< Retry-After: 120

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.

Reviewed 6 min readintermediate2 sourcesTry itMarkdown
Direction
Response
Category
Rate limiting
Spec
RFC 9110 §10.2.3
On this page

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/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:

Retry-After: 60

Wait 60 seconds, then retry.

HTTP Date

A specific date and time to retry after:

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/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/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/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/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/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/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/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:

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:

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

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

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/1.1 429 Too Many Requests
Retry-After: 60

2. Pick realistic wait times:

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

3. Choose the format that fits the situation:

✅ 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:

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

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

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

❌ 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

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.

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.

Sources

  1. MDN Web Docs: Retry-Afterdeveloper.mozilla.org
  2. RFC 9110: Retry-Afterrfc-editor.org

Keep going

Browse /search