Chapter DRate limiting labPage 4 of 8

Rate limiting lab

Inject a controlled rate limiting failure

Production rule: Exercise one bounded failure for a multi-tenant RAG answer API; no stage is complete until another operator can reproduce its evidence and reverse its risky action.

~35 minValidation

Before you start

Why this matters

In two minutes, write the user-visible outcome this page protects, one numerical threshold, and the first signal you expect to move. Then name an observation that would prove your initial theory wrong. Keep the answer beside your terminal; this lab rewards prediction before inspection rather than explanations invented after the graph changes.

1Learn the idea

Read

Lab target

You own a multi-tenant RAG answer API at POST /v1/answers. The goal is to protect shared model and vector-search capacity without punishing normal interactive users. The measurable target is a 60 requests/minute user bucket with a burst of 10, a 1,200 requests/minute tenant ceiling, and a 4,000 requests/minute global guard; rejected requests return HTTP 429 in under 25 ms with Retry-After. The known production tension is strict per-user fairness prevents a noisy neighbor but can block legitimate batch imports; local fallback buckets improve availability but permit bounded overshoot during Redis partitions.

Read

State a falsifiable hypothesis

This page deliberately breaks Rate limiting in a disposable or explicitly approved environment. The scenario is run a retry storm from one tenant at 180 requests/second while Redis latency is injected at 250 ms; verify the global guard remains available and the limiter fails closed only for expensive generation. Write the hypothesis as an ordered signal chain: fault injection, component-level signal, user-visible indicator, protective action, and recovery. Include expected timestamps or maximum delays. “The system becomes unhealthy” is not precise enough to debug.

Capture a baseline of rate_limit_decisions_total{scope,outcome}, rate_limit_redis_seconds, http_requests_total{status="429"}, and allowed request p95 latency and record the effective configuration:

limits:
  user: { rate_per_minute: 60, burst: 10 }
  tenant: { rate_per_minute: 1200, burst: 100 }
  global: { rate_per_minute: 4000, burst: 250 }
backend:
  redis_timeout_ms: 40
  on_timeout: local_bucket
response:
  status: 429
  retry_after: true

Define hard aborts before starting: unexpected data mutation, impact outside the test scope, unbounded cost, missing telemetry, or inability to remove the fault. Name the person who can stop the drill. Confirm the cleanup command and take a state snapshot that proves the environment can be restored.

Read

Inject and contain the fault

Run exactly one fault workflow:

toxiproxy-cli toxic add redis -t latency -a latency=250
k6 run -e RATE=180 scenarios/retry-storm.js

Mark the start time in UTC. Do not tune the system during the first observation window. Compare actual signal order with the hypothesis and note the first divergence. The target remains a 60 requests/minute user bucket with a burst of 10, a 1,200 requests/minute tenant ceiling, and a 4,000 requests/minute global guard; rejected requests return HTTP 429 in under 25 ms with Retry-After. A protection mechanism may intentionally reject or degrade some work; count that as success only if the user contract explicitly permits it and unaffected traffic remains inside its objective.

Expected telemetry must show demand as well as outcomes. A flat error counter during a traffic outage is not resilience. Verify the injector itself worked through an independent observation such as dependency latency, worker count, policy state, or fault-proxy statistics. If no signal moves, stop and diagnose the drill rather than escalating fault intensity blindly.

Read

Remove the fault and prove recovery

Remove the injection at the planned time, preserve logs, and observe several consecutive samples. Check backlog, delayed retries, stale endpoints, cached state, and circuit or escalation state; recovery of the front-door status alone is insufficient. Compare final state to the baseline and explain any residual difference.

Use the historical incident as a reason to be exact: tenant acme-importer shipped a zero-jitter retry loop, consumed 74% of the global model budget, and pushed interactive p95 from 1.8 s to 6.4 s. The production tradeoff is strict per-user fairness prevents a noisy neighbor but can block legitimate batch imports; local fallback buckets improve availability but permit bounded overshoot during Redis partitions. State whether the current behavior contains that risk or merely moves it. The evidence bundle must contain the hypothesis, baseline, fault command, start and stop times, telemetry queries, cleanup proof, and one finding that changes a future design or operating decision.

Checking tutor…