# RetryLab

> Deterministically flaky HTTP endpoints for testing retries, backoff, idempotency and webhooks. One URL that fails exactly the way you tell it to. Free, no sign-up. Benches are wiped every day at 00:00 Africa/Tripoli.

- Base URL: https://retrylab.dev
- Short index: https://retrylab.dev/llms.txt
- OpenAPI 3.1: https://retrylab.dev/openapi.json
- Any page answers in Markdown with `Accept: text/markdown` or `?format=md`; curl, wget, Python, Node and known AI agents get it without asking. Browsers get the interactive bench with a live visualisation.

## Quick start

1. Create a bench. A bench is a private endpoint with its own settings; its id is the only credential, so keep it to yourself.

        curl -s -X POST https://retrylab.dev/api/sessions \
          -H "Content-Type: application/json" \
          -d '{"name":"checkout","method":"POST","fail":3,"code":503}'

   The 201 response:

        {
          "id": "<uuid>",
          "name": "checkout",
          "method": "POST",
          "endpoint": "https://retrylab.dev/api/e/<uuid>/checkout",
          "endpoint_with_settings": "https://retrylab.dev/api/e/<uuid>/checkout?fail=3&code=503",
          "bench": "https://retrylab.dev/<uuid>",
          "settings": {"fail":3,"code":503,"percent":0,"latency":0,"jitter":0,"timeout":0,"drop":false,"echo":false},
          "stats": {"requests":0,"last_request_at":null},
          "created_at": "...", "expires_at": "2026-10-20T00:00:00+02:00",
          "docs": "https://retrylab.dev/llms.txt", "openapi": "https://retrylab.dev/openapi.json"
        }

   Every field in the request body is optional. With an empty body you get `GET https://retrylab.dev/api/e/<uuid>/test` that always answers 200.

2. Call the endpoint. With the settings above the first three calls answer 503 and the fourth answers 200:

        curl -i -X POST "https://retrylab.dev/api/e/<uuid>/checkout" -H "X-Idempotency-Key: order-1"

3. Vary the behaviour per request with query parameters, which override the bench settings for that one call, or change the bench itself:

        curl -i -X POST "https://retrylab.dev/api/e/<uuid>/checkout?percent=35&latency=800&jitter=200"
        curl -s -X PATCH https://retrylab.dev/api/sessions/<uuid> -H "Content-Type: application/json" -d '{"timeout":3000}'

GET-only clients: `GET https://retrylab.dev/new` answers 302 with `Location: https://retrylab.dev/<uuid>`. That bench's endpoint is `GET https://retrylab.dev/api/e/<uuid>/test` until its name or method is changed. `GET https://retrylab.dev/<uuid>` (as Markdown) shows a bench's settings, stats and ready-made calls; visiting a random version-4 UUID that does not exist yet creates it, any other id answers 404.

## The fault endpoint

    {METHOD} https://retrylab.dev/api/e/{id}/{name}[?parameters]

- `{METHOD}` must be the bench's method (GET, POST, PUT, PATCH, DELETE). Anything else answers 405 with `expected` and `received` in the body.
- `{name}` must be the bench's endpoint name, otherwise 404. The body says which name the bench has.
- Every call is counted on the bench and answers JSON, unless it times out or drops.

### Query parameters

| parameter | effect | range |
|---|---|---|
| fail | Fail this many attempts, then succeed. Counted per token. | 0 to 100 |
| code | Status returned on a failure. 200 means failures still answer OK. | 200 400 401 403 404 422 429 500 502 503 504 |
| percent | Chance, in percent, that any given request fails. | 0 to 100 |
| latency | Milliseconds to wait before answering. | 0 to 30000 |
| jitter | Randomises latency by up to this many milliseconds either way. | 0 to 5000 |
| timeout | Hold the connection this long, then close it with no response. | 0 to 60000 |
| drop | Close the connection immediately, no response. | 0 or 1 |
| echo | Include method, headers, query and body in a successful answer. | 0 or 1 |
| retry_after | Add a Retry-After header with this many seconds to failure responses. | 0 to 3600 |
| token | Key for the attempt counter. Falls back to the X-Idempotency-Key header. | any string |

Attempt counters: every token has its own counter. A call with neither `token` nor `X-Idempotency-Key` uses the endpoint's one shared counter, named `shared` in responses, so a plain curl loop still sees fail, fail, fail, success. Send a token per client when you need independent counters. The client IP is deliberately not used: many networks change it between connections, which made IP-keyed counters restart on every call. Counters live 30 minutes after the last hit; resetting the counter on the bench page clears them. Timeouts and drops never answer, so they do not advance a counter.

Order of precedence: latency, then timeout, then drop, then scripted failures (fail), then random failures (percent).

Parameters are checked, not coerced: a value the endpoint does not understand (`fail=abc`, `code=418`, `drop=maybe`, `latency=99999`) answers 400 with `error`, `parameter`, `hint` and `docs`, so a typo can never look like a passing test. `drop` and `echo` accept 1/0 and true/false.

### Responses

- Success, status 200: `{"ok":true,"attempt":4,"name":"checkout","session":"<uuid>","token":"order-1"}`. With echo=1 the object also carries `method`, `headers`, `query`, `json`, `form` and `raw`.
- Scripted failure, status = code: `{"ok":false,"reason":"scripted-failure","attempt":1,"token":"order-1"}`
- Random failure, status = code: `{"ok":false,"reason":"random-failure-35%","attempt":7,"token":"shared"}`
- Timeout: the connection is held open for `timeout` milliseconds and then closed with no status line, no headers and no body. curl reports "empty reply from server" (exit 52); Python raises RemoteDisconnected. Your client's own timeout should normally fire first.
- Drop: the connection is closed at once in the same way.
- 400, 404, 405, 429: JSON with `error`, `hint` and `docs`.

## Session API

JSON in, JSON out. Field names are the same as the query parameters above, plus `name` and `method`.

| call | purpose |
|---|---|
| `POST https://retrylab.dev/api/sessions` | Create a bench. Body fields, all optional: name, method, fail, code, percent, latency, jitter, timeout, drop, echo. Answers 201 with the bench. |
| `GET https://retrylab.dev/api/sessions/{id}` | The bench, its settings and stats (`requests`, `last_request_at`). |
| `PATCH https://retrylab.dev/api/sessions/{id}` | Change any of the same fields. Answers 200 with the bench. |

Validation failures answer 422 with `message` and `errors` keyed by field. A body that is not valid JSON answers 400. Unknown ids answer 404; ids are case-insensitive.

## Limits

- 120 requests a minute per IP on the fault endpoint and the session API; beyond that, 429.
- 10 new benches a minute per IP; beyond that 429, and a temporary 403 block if it keeps going. One bench serves any number of requests, so reuse it.
- Benches, their settings and their counters are wiped every day at 00:00 Africa/Tripoli. The next wipe is 2026-10-20T00:00:00+02:00.

## Human interface

Open any bench URL in a browser for a control deck and a live visualisation of the configured behaviour. Source and issues: https://github.com/Binary-ly/retrylab-community
