Skip to main content

Retry and Backoff Behavior

When running API test suites, transient failures are a reality — a server might be momentarily overloaded, a database lock might not yet be released, or a resource might not have finished being created yet. To handle these situations gracefully, Rumour provides a built-in retry system with exponential backoff.

Configuring Retries

Add the retries key at the top level of your .toml request file:

name = "get_user"

retries = 2

[request]
method = "GET"
url = "{{base_url}}/users/1"

[assert]
status = 200

This tells Rumour to try the request up to 3 times total — the original attempt plus 2 retries.

note

If retries is not specified, Rumour defaults to 2 retries for requests inside a suite workflow. For single-file runs, it defaults to 0 unless specified.

When Does a Retry Trigger?

A retry is triggered whenever a request execution ends in failure, meaning either:

  1. The HTTP request itself fails at the transport level (e.g., connection refused)
  2. The request succeeds at the network level but one or more assertions fail (e.g., wrong status code, wrong JSON value)

In both cases, Rumour waits for the configured backoff delay and then re-runs the entire request from scratch — including re-resolving all variables.

Exponential Backoff Delay

To avoid hammering a struggling server, Rumour does not retry immediately. It waits progressively longer between each attempt using an exponential backoff formula:

Delay = 500ms × 2^(attempt - 1)
AttemptWait Before Running
1st retry500ms
2nd retry1,000ms (1 second)
3rd retry2,000ms (2 seconds)
4th retry4,000ms
5th retry8,000ms
......
10th+ retry512,000ms (capped at ~8.5 minutes)

The delay is capped at 512,000ms to prevent runaway wait times on high retry counts.

Example

Request File

name = "retry_example"

retries = 2

[request]
method = "GET"
url = "{{base_url}}/users/9999"

[assert]
status = 200

[assert.json."id"]
equal = 9999

Run

rumour run ./retry_demo.toml -v

Output

This request targets a user ID (9999) that does not exist on the mock server, so it fails its assertion on every attempt. You can see Rumour retrying automatically before finally reporting failure:

GET http://localhost:3000/users/9999
✗ FAILED: retry_demo.toml (1ms)
↻ RETRYING: retry_demo.toml (Attempt 1/2) [Wait 500ms] ...
✗ FAILED: retry_demo.toml (1ms)
↻ RETRYING: retry_demo.toml (Attempt 2/2) [Wait 1000ms] ...
✗ FAILED: retry_demo.toml (1ms)

╭──────────────────────────────────────────────────────────────────────────╮
│ RUMOUR EXECUTION REPORT │
├──────────────────────────────────────────────────────────────────────────┤
│ Total Requests: 1
│ Successful: 0
│ Failed: 1
│ Skipped: 0
│ Success Rate: 0.0% │
│ Total Time: 1509ms │
╰──────────────────────────────────────────────────────────────────────────╯

✗ Failed Requests:
- retry_demo.toml: [200]
Reason: JSON Body: Failed to parse body as JSON for assertions

Notice the Total Time: 1509ms — roughly 1,500ms of wait time across the two retries (500ms + 1,000ms), confirming exponential backoff is working correctly.

Retries Are Always Sequential

Even when running a suite in parallel mode (-p), retries for a single request node are always sequential. Rumour cannot schedule the next attempt until the current one finishes and fails. This is by design — retrying concurrently would multiply the load on an already-struggling server.

Best Practices

Keep retries between 2 and 5

For typical API tests, retries = 2 or retries = 3 is sufficient to handle transient failures. Very high retry counts (e.g., retries = 20) should be avoided because:

  • They significantly increase total runtime if a service is persistently down
  • With the backoff cap of 512,000ms, retries = 20 means a total wait of over 90 minutes in the worst case

Don't use retries to mask bugs

Retries should handle transient, expected failures like eventual consistency. If a request consistently fails, the root cause is a problem in your API or test setup — not something retries should paper over.

Combine with the skip directive

If a request's failure is acceptable (e.g., an optional cleanup step), use skip = true instead of a high retry count:

skip = true

[request]
method = "DELETE"
url = "{{base_url}}/items/{{item_id}}"

Retries in Parallel Execution

When running a suite with -p, multiple request nodes execute concurrently. Each node manages its own retry loop independently. If node A fails and is retrying, node B (which doesn't depend on A) continues executing at full speed — they don't block each other.

Only nodes that declare an explicit dependency on a failing node will be held until that node either eventually succeeds or exhausts all retries and is marked as failed.