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.
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:
- The HTTP request itself fails at the transport level (e.g., connection refused)
- 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)
| Attempt | Wait Before Running |
|---|---|
| 1st retry | 500ms |
| 2nd retry | 1,000ms (1 second) |
| 3rd retry | 2,000ms (2 seconds) |
| 4th retry | 4,000ms |
| 5th retry | 8,000ms |
| ... | ... |
| 10th+ retry | 512,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 = 20means 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.