# Rate Limiting and Resilience

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Production-Ready APIs
Source: https://www.devtutorials.ai/learn/building-apis/production-ready/rate-limits-and-resilience

> Protect your API from too much traffic with rate limits and 429 Too Many Requests, and make clients cope with slow or failing services using retries and backoff.

**Rate limiting** controls how many requests a caller can make within a period of time.

**Resilience** helps applications continue working when services are slow, overloaded, or temporarily unavailable. Instead of failing immediately, a resilient client waits, retries carefully, and avoids making a bad situation worse. Together, rate limiting and resilience protect both the API and its users.

## Watch One Client Flood the API

A partner application contains a bug. Instead of loading the menu once, it repeatedly calls `GET /menu` in a loop. Press **Run**.

Results:

- All six requests receive `200 OK`.
- `/stats` reports: `{"requests": 6}`

Nothing slows the client down. Nothing protects the API. A small bug may generate thousands or millions of requests. This can increase costs, slow responses, overload servers, and affect other customers.

**In simple words:** the API needs rules that prevent one caller from using unlimited resources.

## TL;DR

- A **rate limit** restricts how many requests a caller may make during a time period.
- Going over the limit should return `429 Too Many Requests`.
- Include the `Retry-After` header, to tell the client when it may try again.
- Apply limits per user, API key, or client.
- Fixed windows are simple but can be bursty.
- Token buckets handle bursts more smoothly.
- Retry temporary failures using exponential backoff and jitter.
- Never retry `400 Bad Request`, and never retry non-idempotent POST operations without an Idempotency-Key.

## A Real-World Example

Imagine a shop selling a limited-edition product. A sign at the entrance says: `One per customer`.

The goal is fairness. Without the rule, one person buys everything, and everyone else gets nothing. Rate limiting serves the same purpose. It ensures fair usage, stable performance, and protection from bugs and abuse.

## What You Will Learn

In this lesson, you will learn:

1. Why APIs use rate limits.
2. How fixed-window rate limiting works.
3. How to limit requests per API key.
4. How token bucket rate limiting handles bursts.
5. How resilient clients retry safely.
6. When retries should and should not happen.

## A Fixed Window Rate Limit

A fixed-window limiter counts requests during a fixed period. Example rule: `3 requests every 60 seconds`. The first three requests succeed. The fourth request exceeds the limit.

### Requests Within the Limit

At `10s`, `20s` and `30s`, the responses are `200 OK`. The remaining requests decrease: `2`, `1`, `0`.

### Request Over the Limit

At `40s`, the response is `429 Too Many Requests` with `Retry-After: 20`. The client must wait until the 60-second window ends.

### New Window

At `61s`, the counter resets. Requests are allowed again, and the response is `200 OK`.

A `429 Too Many Requests` response is normal behavior. Well-behaved clients read the `Retry-After` header, pause their requests, and try again later.

## Rate Limits Per API Key

Rate limits should apply to each caller separately. The activity of one user should not affect the access of another user.

Example:

### Asha's Key

The limit is `3 requests`. The results are `200`, `200`, `200`, `429`. The response includes `Retry-After: 52`.

### Swiggo's Key

It uses a completely separate counter. Even after Asha reaches her limit, `200 OK` still succeeds for Swiggo.

### Missing Key

The API cannot identify the caller, so the response is `401 Unauthorized`. Without an identity, limits cannot be enforced properly.

## Token Bucket Rate Limiting

Fixed windows have a weakness. A caller could send `3 requests at second 59` and `3 requests at second 60`, and effectively make six requests almost at once.

A **token bucket** gives smoother behavior.

### How It Works

The bucket stores tokens. Example: `Capacity: 3 tokens` and `Refill: 1 token per second`

Each request uses one token. No token means no request.

Example:

### Second 0

The bucket has `3 tokens`. The results are `✓ ✓ ✓ ✗`.

### Second 1

One token returns. The result is `✓`.

### Second 2

One more token returns. The results are `✓ ✗ ✗`.

### Second 6

The bucket has refilled completely. Another burst is allowed.

Token buckets allow occasional bursts while they enforce a long-term rate.

## Resilience and Retries

Sometimes requests fail because of temporary problems, such as network issues, timeouts, service restarts, and short outages. Retrying immediately can make the situation worse. Instead, clients should retry gradually.

### Exponential Backoff

Wait longer after each failure. Example: `Attempt 1 → wait 100ms`, `Attempt 2 → wait 200ms`, `Attempt 3 → wait 400ms`

The delay doubles each time.

### Jitter

Add a small random delay. Example: `107ms`, `223ms`, `412ms`

This prevents thousands of clients from retrying at the same moment.

Example:

### Timeout

Attempt 1 ends with `Timeout`, and the client waits `107ms`. Attempt 2 ends with `Timeout`, and the client waits `223ms`. Attempt 3 returns `200 OK`, which is a success.

### Retry-After

If the server returns `429 Too Many Requests` with `Retry-After: 2`, the client waits `2000ms`.

The instruction of the server takes priority over the calculated backoff delay.

## Circuit Breakers

Sometimes retries are not enough. A service may stay unavailable for a long time. Continuing to send requests only creates more load.

A **circuit breaker** stops requests for a while.

Typical behavior:

1. Several failures occur.
2. The circuit opens.
3. Requests fail immediately.
4. After a waiting period, one test request is allowed.
5. If it succeeds, normal traffic resumes.

The benefits are that it protects struggling services, reduces unnecessary traffic, and recovers more quickly after outages.

## When You Should Not Retry

Some failures will never succeed if they are retried.

### Do Not Retry Validation Errors

Example: `400 Bad Request`

The request itself is invalid. Repeating it produces the same result.

### Be Careful with POST Requests

Consider `POST /payments`. The payment may have succeeded even if the response was lost. A blind retry may charge the customer twice.

Only retry POST requests when they use an `Idempotency-Key`, which makes sure that duplicate requests are handled safely.

Never retry a payment or a similar operation unless the request is idempotent. Retries should be safe, predictable, and controlled.

## Rate Limiting in ASP.NET Core

Real ASP.NET Core applications include built-in rate limiting support. Common limiter types include Fixed Window, Sliding Window, and Token Bucket.

Rate limits can be partitioned by API key, user, IP address, or custom identifiers. Applications can also configure `429` responses, retry information, and different limits for different endpoints.

## How to Choose a Limit

Rate limits should be based on real usage patterns. Example: if normal users make `20 requests per minute`, then `100 requests per minute` might be a reasonable limit.

Limits that are too strict frustrate legitimate users. Limits that are too high give little protection. Monitor usage before you decide.

Set limits using real production data, not guesses. Review traffic patterns regularly, and change the limits when necessary.

## Where You See This

- Public APIs return `429 Too Many Requests` when limits are exceeded.
- Login systems limit failed sign-in attempts.
- Mobile apps retry temporary network failures.
- Cloud SDKs use backoff and jitter automatically.
- Payment APIs rely on idempotency keys for safe retries.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| One client slows the API for everyone | No rate limiting | Add limits per user or API key |
| Clients keep receiving `429` repeatedly | `Retry-After` is ignored | Wait the specified amount of time |
| Customers are charged twice | POST requests are retried unsafely | Use Idempotency-Key or avoid retries |
| Outages become worse under load | Clients retry immediately | Use backoff, jitter, and circuit breakers |
| Invalid requests are retried repeatedly | `400` responses are treated as temporary failures | Retry only transient failures such as `429`, `503`, or timeouts |
| Legitimate users are blocked too often | Limits are too low | Review real usage and adjust limits |

## Exercises

## What to Remember

- A **rate limit** stops one caller from taking everything. Count **per key**.
- When a caller is over the limit, send `429 Too Many Requests` with **`Retry-After`** and a Problem Details body.
- A **fixed window** is simple. A **token bucket** allows short bursts and fills up slowly again.
- Clients retry temporary failures with **exponential backoff** and **jitter**. They also obey `Retry-After`.
- Never retry a `400`, and never retry a POST without an **Idempotency-Key**.

**Next:** API Monitoring and Logging. You will learn how to watch your API in production, so that you see the floods, the 429s and the slow calls before your customers do.

var app = builder.Build();

// A simple counter so we can see how many requests reached the API.
int requests = 0;

app.MapGet("/menu", () =>
{
    requests++;
    return Results.Ok(new[] { new { name = "Latte", price = 220 }, new { name = "Masala Chai", price = 120 } });
});

// Nothing here stops a client that calls again and again.
app.MapGet("/stats", () => Results.Ok(new { requests }));

app.Run();`;

const app = new Hono();

// A simple counter so we can see how many requests reached the API.
let requests = 0;

app.get("/menu", (c) => {
  requests++;
  return c.json([{ name: "Latte", price: 220 }, { name: "Masala Chai", price: 120 }], 200);
});

// Nothing here stops a client that calls again and again.
app.get("/stats", (c) => c.json({ requests }, 200));

###
GET /menu

###
GET /menu

###
GET /menu

###
GET /menu

###
GET /menu

###
GET /stats`;

var app = builder.Build();

const int Limit = 3;          // 3 requests...
const int WindowSeconds = 60; // ...per 60 seconds, per API key
var counters = new Dictionary<string, (long Window, int Count)>();

app.MapGet("/menu", (HttpContext context) =>
{
    string key = context.Request.Headers["X-Api-Key"].ToString();
    // Real limiters read the clock. The demo sends the time so every run is the same.
    long now = long.Parse(context.Request.Headers["X-Demo-Time"].ToString());
    long window = now / WindowSeconds;

    var (w, count) = counters.GetValueOrDefault(key);
    if (w != window) count = 0;       // a new window starts with an empty counter
    if (count >= Limit)
    {
        long secondsLeft = (window + 1) * WindowSeconds - now;
        context.Response.Headers["Retry-After"] = secondsLeft.ToString();
        return Results.Problem(statusCode: 429, title: "Too Many Requests",
            type: "https://tools.ietf.org/html/rfc6585#section-4",
            detail: "Limit is 3 requests per 60 seconds. Try again in " + secondsLeft + " seconds.");
    }
    counters[key] = (window, count + 1);
    return Results.Ok(new { items = new[] { "Latte", "Masala Chai" }, remaining = Limit - count - 1 });
});

app.Run();`;

const LIMIT = 3;           // 3 requests...
const WINDOW_SECONDS = 60; // ...per 60 seconds, per API key
const counters = new Map<string, { window: number; count: number }>();

const app = new Hono();

app.get("/menu", (c) => {
  const key = c.req.header("X-Api-Key") ?? "";
  // Real limiters read the clock. The demo sends the time so every run is the same.
  const now = Number(c.req.header("X-Demo-Time"));
  const window = Math.floor(now / WINDOW_SECONDS);

  const entry = counters.get(key);
  const count = entry && entry.window === window ? entry.count : 0; // a new window starts empty
  if (count >= LIMIT) {
    const secondsLeft = (window + 1) * WINDOW_SECONDS - now;
    c.header("Retry-After", String(secondsLeft));
    const body = {
      type: "https://tools.ietf.org/html/rfc6585#section-4", title: "Too Many Requests", status: 429,
      detail: "Limit is 3 requests per 60 seconds. Try again in " + secondsLeft + " seconds.",
    };
    return c.body(JSON.stringify(body), 429, { "Content-Type": "application/problem+json" });
  }
  counters.set(key, { window, count: count + 1 });
  return c.json({ items: ["Latte", "Masala Chai"], remaining: LIMIT - count - 1 }, 200);
});

X-Api-Key: key-asha
X-Demo-Time: 10

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 20

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 30

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 40

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 61`;

var app = builder.Build();

// Each API key has its own plan and its own counter.
var plans = new Dictionary<string, int> { ["key-asha"] = 3, ["key-swiggo"] = 10 };
var counters = new Dictionary<string, (long Window, int Count)>();

app.MapGet("/menu", (HttpContext context) =>
{
    string key = context.Request.Headers["X-Api-Key"].ToString();
    if (!plans.TryGetValue(key, out int limit)) return Results.StatusCode(401);

    long now = long.Parse(context.Request.Headers["X-Demo-Time"].ToString());
    long window = now / 60;
    var (w, count) = counters.GetValueOrDefault(key);
    if (w != window) count = 0;
    if (count >= limit)
    {
        context.Response.Headers["Retry-After"] = ((window + 1) * 60 - now).ToString();
        return Results.Problem(statusCode: 429, title: "Too Many Requests",
            type: "https://tools.ietf.org/html/rfc6585#section-4",
            detail: "Key " + key + " allows " + limit + " requests per 60 seconds.");
    }
    counters[key] = (window, count + 1);
    return Results.Ok(new { key, used = count + 1, limit });
});

app.Run();`;

// Each API key has its own plan and its own counter.
const plans = new Map<string, number>([["key-asha", 3], ["key-swiggo", 10]]);
const counters = new Map<string, { window: number; count: number }>();

const app = new Hono();

app.get("/menu", (c) => {
  const key = c.req.header("X-Api-Key") ?? "";
  const limit = plans.get(key);
  if (limit === undefined) return c.body(null, 401);

  const now = Number(c.req.header("X-Demo-Time"));
  const window = Math.floor(now / 60);
  const entry = counters.get(key);
  const count = entry && entry.window === window ? entry.count : 0;
  if (count >= limit) {
    c.header("Retry-After", String((window + 1) * 60 - now));
    const body = {
      type: "https://tools.ietf.org/html/rfc6585#section-4", title: "Too Many Requests", status: 429,
      detail: "Key " + key + " allows " + limit + " requests per 60 seconds.",
    };
    return c.body(JSON.stringify(body), 429, { "Content-Type": "application/problem+json" });
  }
  counters.set(key, { window, count: count + 1 });
  return c.json({ key, used: count + 1, limit }, 200);
});

X-Api-Key: key-asha
X-Demo-Time: 5

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 6

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 7

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 8

###
GET /menu
X-Api-Key: key-swiggo
X-Demo-Time: 9

###
GET /menu
X-Demo-Time: 10`;

// One new token drips in every second, up to the capacity.
const int Capacity = 3;
const int RefillPerSecond = 1;

int tokens = Capacity;
int lastTime = 0;
int[] requestTimes = { 0, 0, 0, 0, 1, 2, 2, 2, 6, 6, 6, 6 };

foreach (int t in requestTimes)
{
    // Add the tokens that dripped in since the last request.
    tokens = Math.Min(Capacity, tokens + (t - lastTime) * RefillPerSecond);
    lastTime = t;

    if (tokens > 0)
    {
        tokens--;
        Console.WriteLine($"t={t}s  allowed  (tokens left: {tokens})");
    }
    else
    {
        Console.WriteLine($"t={t}s  BLOCKED  429 (bucket empty)");
    }
}`;

// One new token drips in every second, up to the capacity.
const CAPACITY = 3;
const REFILL_PER_SECOND = 1;

let tokens = CAPACITY;
let lastTime = 0;
const requestTimes = [0, 0, 0, 0, 1, 2, 2, 2, 6, 6, 6, 6];

for (const t of requestTimes) {
  // Add the tokens that dripped in since the last request.
  tokens = Math.min(CAPACITY, tokens + (t - lastTime) * REFILL_PER_SECOND);
  lastTime = t;

  if (tokens > 0) {
    tokens--;
    console.log(\`t=\${t}s  allowed  (tokens left: \${tokens})\`);
  } else {
    console.log(\`t=\${t}s  BLOCKED  429 (bucket empty)\`);
  }
}`;

// Real code would call HttpClient. Here every run gives the same result.
string[] paymentReplies = { "timeout", "timeout", "200 OK" };
int[] jitter = { 7, 23, 11 }; // a small fixed "random" extra, so clients do not retry in lockstep

Console.WriteLine("Payment for order 101 (Idempotency-Key: pay-101):");
for (int attempt = 0; attempt < 4; attempt++)
{
    string reply = paymentReplies[attempt];
    Console.WriteLine($"  Try {attempt + 1}: {reply}");
    if (reply == "200 OK") break;
    int wait = 100 * (1 << attempt) + jitter[attempt]; // 100, 200, 400 ms + jitter
    Console.WriteLine($"  -> wait {wait} ms, then retry");
}

// When the server says how long to wait, believe it.
Console.WriteLine("Menu call that gets 429 with Retry-After: 2");
int? retryAfterSeconds = 2;
int backoff = 100 + jitter[0];
int chosen = retryAfterSeconds is int s ? s * 1000 : backoff;
Console.WriteLine($"  -> wait {chosen} ms (Retry-After wins over backoff {backoff} ms)");`;

// Real code would call fetch. Here every run gives the same result.
const paymentReplies = ["timeout", "timeout", "200 OK"];
const jitter = [7, 23, 11]; // a small fixed "random" extra, so clients do not retry in lockstep

console.log("Payment for order 101 (Idempotency-Key: pay-101):");
for (let attempt = 0; attempt < 4; attempt++) {
  const reply = paymentReplies[attempt];
  console.log(\`  Try \${attempt + 1}: \${reply}\`);
  if (reply === "200 OK") break;
  const wait = 100 * 2 ** attempt + jitter[attempt]; // 100, 200, 400 ms + jitter
  console.log(\`  -> wait \${wait} ms, then retry\`);
}

// When the server says how long to wait, believe it.
console.log("Menu call that gets 429 with Retry-After: 2");
const retryAfterSeconds: number | null = 2;
const backoff = 100 + jitter[0];
const chosen = retryAfterSeconds !== null ? retryAfterSeconds * 1000 : backoff;
console.log(\`  -> wait \${chosen} ms (Retry-After wins over backoff \${backoff} ms)\`);`;

  {
    prompt: "The retry loop waits 100 ms every time. Change it so that try n waits 100 * 2^n ms, but never more than 2000 ms.",
    hint: "Double the wait with 1 << n in C# or 2 ** n in TypeScript. Then take the smaller of that number and 2000.",
    explain: "Exponential backoff gives a struggling service more and more time to recover. The cap stops the wait from growing forever: tries 5 and 6 both wait 2000 ms.",
    csharp: { starter: `// Exponential backoff: wait 100 * 2^n ms before retry n, but never more than 2000 ms.
// TODO: this waits the same 100 ms every time. Double it each try and cap it at 2000.
const int BaseMs = 100;
const int CapMs = 2000;

for (int n = 0; n <= 6; n++)
{
    int wait = BaseMs;
    Console.WriteLine($"Retry {n}: wait {wait} ms (cap {CapMs})");
}`, solution: `// Exponential backoff: wait 100 * 2^n ms before retry n, but never more than 2000 ms.
const int BaseMs = 100;
const int CapMs = 2000;

for (int n = 0; n <= 6; n++)
{
    int wait = Math.Min(CapMs, BaseMs * (1 << n)); // double each time, then stop growing
    Console.WriteLine($"Retry {n}: wait {wait} ms (cap {CapMs})");
}`, mode: "console" },
    react: { starter: `// Exponential backoff: wait 100 * 2^n ms before retry n, but never more than 2000 ms.
// TODO: this waits the same 100 ms every time. Double it each try and cap it at 2000.
const BASE_MS = 100;
const CAP_MS = 2000;

for (let n = 0; n <= 6; n++) {
  const wait = BASE_MS;
  console.log(\`Retry \${n}: wait \${wait} ms (cap \${CAP_MS})\`);
}`, solution: `// Exponential backoff: wait 100 * 2^n ms before retry n, but never more than 2000 ms.
const BASE_MS = 100;
const CAP_MS = 2000;

for (let n = 0; n <= 6; n++) {
  const wait = Math.min(CAP_MS, BASE_MS * 2 ** n); // double each time, then stop growing
  console.log(\`Retry \${n}: wait \${wait} ms (cap \${CAP_MS})\`);
}`, mode: "console" }
  },
  {
    prompt: "GET /menu has no limit. Allow 2 requests per 60 seconds for each X-Api-Key. The 3rd request in a window must get 429 with Retry-After and a Problem Details body.",
    hint: "Keep a dictionary from key to (window, count). The window is X-Demo-Time / 60. Retry-After is (window + 1) * 60 - now.",
    explain: "The requests at 5 and 10 seconds pass. The request at 15 seconds is the 3rd one in the window, so it gets 429 with Retry-After: 45. At 60 seconds a new window starts, and the request passes again.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// TODO: allow only 2 requests per 60 seconds for each X-Api-Key.
// Use the X-Demo-Time header as "now" (in seconds). The window is now / 60.
// The 3rd request in a window gets 429 with a Retry-After header
// (seconds left in the window) and a Problem Details body.
app.MapGet("/menu", (HttpContext context) =>
{
    return Results.Ok(new { items = new[] { "Latte", "Cold Coffee" } });
});

app.Run();`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var counters = new Dictionary<string, (long Window, int Count)>();

app.MapGet("/menu", (HttpContext context) =>
{
    string key = context.Request.Headers["X-Api-Key"].ToString();
    long now = long.Parse(context.Request.Headers["X-Demo-Time"].ToString());
    long window = now / 60;

    var (w, count) = counters.GetValueOrDefault(key);
    if (w != window) count = 0;
    if (count >= 2)
    {
        context.Response.Headers["Retry-After"] = ((window + 1) * 60 - now).ToString();
        return Results.Problem(statusCode: 429, title: "Too Many Requests",
            type: "https://tools.ietf.org/html/rfc6585#section-4",
            detail: "Limit is 2 requests per 60 seconds.");
    }
    counters[key] = (window, count + 1);
    return Results.Ok(new { items = new[] { "Latte", "Cold Coffee" } });
});

app.Run();`, mode: "aspnet", requests: `GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 5

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 10

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 15

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 60` },
    react: { starter: `import { Hono } from "hono";

const app = new Hono();

// TODO: allow only 2 requests per 60 seconds for each X-Api-Key.
// Use the X-Demo-Time header as "now" (in seconds). The window is now / 60.
// The 3rd request in a window gets 429 with a Retry-After header
// (seconds left in the window) and a Problem Details body.
app.get("/menu", (c) => {
  return c.json({ items: ["Latte", "Cold Coffee"] }, 200);
});

const counters = new Map<string, { window: number; count: number }>();

const app = new Hono();

app.get("/menu", (c) => {
  const key = c.req.header("X-Api-Key") ?? "";
  const now = Number(c.req.header("X-Demo-Time"));
  const window = Math.floor(now / 60);

  const entry = counters.get(key);
  const count = entry && entry.window === window ? entry.count : 0;
  if (count >= 2) {
    c.header("Retry-After", String((window + 1) * 60 - now));
    const body = {
      type: "https://tools.ietf.org/html/rfc6585#section-4", title: "Too Many Requests", status: 429,
      detail: "Limit is 2 requests per 60 seconds.",
    };
    return c.body(JSON.stringify(body), 429, { "Content-Type": "application/problem+json" });
  }
  counters.set(key, { window, count: count + 1 });
  return c.json({ items: ["Latte", "Cold Coffee"] }, 200);
});

X-Api-Key: key-ravi
X-Demo-Time: 5

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 10

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 15

###
GET /menu
X-Api-Key: key-ravi
X-Demo-Time: 60` },
    checks: `GET /menu
X-Api-Key: key-asha
X-Demo-Time: 61

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 62

###
GET /menu
X-Api-Key: key-asha
X-Demo-Time: 100`
  },
  {
    prompt: "The client always uses backoff, and it even retries a 400. Make Retry-After win over backoff, and stop at a 400 without retrying.",
    hint: "Check for 400 first, and break out of the loop. Then use retryAfter * 1000 when it is not null. Otherwise use 100 * 2^n.",
    explain: "A 429 with Retry-After: 3 now waits 3000 ms, as the server asked. A 400 means the request itself is wrong, so sending it again would fail again. The loop stops there.",
    csharp: { starter: `// Fake replies from the server, one per try: status code and Retry-After (seconds, or null).
var replies = new (int Status, int? RetryAfter)[] { (503, null), (429, 3), (503, null), (400, null), (200, null) };

// TODO 1: if the reply has Retry-After, wait that many seconds (in ms) instead of the backoff.
// TODO 2: a 400 is the client's own mistake. Retrying will not help, so stop and do not retry.
for (int n = 0; n < replies.Length; n++)
{
    var (status, retryAfter) = replies[n];
    if (status == 200) { Console.WriteLine($"Try {n + 1}: 200 -> done"); break; }
    int wait = 100 * (1 << n);
    Console.WriteLine($"Try {n + 1}: {status} -> wait {wait} ms");
}`, solution: `// Fake replies from the server, one per try: status code and Retry-After (seconds, or null).
var replies = new (int Status, int? RetryAfter)[] { (503, null), (429, 3), (503, null), (400, null), (200, null) };

for (int n = 0; n < replies.Length; n++)
{
    var (status, retryAfter) = replies[n];
    if (status == 200) { Console.WriteLine($"Try {n + 1}: 200 -> done"); break; }
    if (status == 400) { Console.WriteLine($"Try {n + 1}: 400 -> stop, fix the request"); break; }
    // Retry-After from the server wins. Otherwise use exponential backoff.
    int wait = retryAfter is int s ? s * 1000 : 100 * (1 << n);
    Console.WriteLine($"Try {n + 1}: {status} -> wait {wait} ms");
}`, mode: "console" },
    react: { starter: `// Fake replies from the server, one per try: status code and Retry-After (seconds, or null).
const replies: { status: number; retryAfter: number | null }[] = [
  { status: 503, retryAfter: null }, { status: 429, retryAfter: 3 }, { status: 503, retryAfter: null },
  { status: 400, retryAfter: null }, { status: 200, retryAfter: null },
];

// TODO 1: if the reply has Retry-After, wait that many seconds (in ms) instead of the backoff.
// TODO 2: a 400 is the client's own mistake. Retrying will not help, so stop and do not retry.
for (let n = 0; n < replies.length; n++) {
  const { status } = replies[n];
  if (status === 200) { console.log(\`Try \${n + 1}: 200 -> done\`); break; }
  const wait = 100 * 2 ** n;
  console.log(\`Try \${n + 1}: \${status} -> wait \${wait} ms\`);
}`, solution: `// Fake replies from the server, one per try: status code and Retry-After (seconds, or null).
const replies: { status: number; retryAfter: number | null }[] = [
  { status: 503, retryAfter: null }, { status: 429, retryAfter: 3 }, { status: 503, retryAfter: null },
  { status: 400, retryAfter: null }, { status: 200, retryAfter: null },
];

for (let n = 0; n < replies.length; n++) {
  const { status, retryAfter } = replies[n];
  if (status === 200) { console.log(\`Try \${n + 1}: 200 -> done\`); break; }
  if (status === 400) { console.log(\`Try \${n + 1}: 400 -> stop, fix the request\`); break; }
  // Retry-After from the server wins. Otherwise use exponential backoff.
  const wait = retryAfter !== null ? retryAfter * 1000 : 100 * 2 ** n;
  console.log(\`Try \${n + 1}: \${status} -> wait \${wait} ms\`);
}`, mode: "console" }
  }
];
