Some APIs are asynchronous. You POST an order and get back 202 Accepted. The order is queued, processed in the background, and some seconds later its status changes from pending to completed. A test that asserts status == 'completed' immediately after the POST will fail — the processing hasn't happened yet. The right tool is polling: send the GET repeatedly on an interval until the condition is met or a timeout expires. Karate has first-class support for this with the retry until keyword.
retry until — the polling keyword
Scenario: Wait for an order to complete processing
# Step 1: trigger the async operation
Given url baseUrl
And path 'orders'
And request { productId: 42, quantity: 2 }
When method post
Then status 202
* def orderId = response.id
# Step 2: poll until the status changes
* configure retry = { count: 10, interval: 2000 }
Given path 'orders', orderId
And retry until response.status == 'completed'
When method get
Then status 200
And match response.status == 'completed'retry until response.status == 'completed' tells Karate to re-send the method get call up to count times (10 in this case), waiting interval milliseconds (2000ms = 2 seconds) between each attempt. When the condition is true, execution continues. When the retry limit is exhausted without the condition becoming true, the scenario fails with a clear message.
Configuring retry
* configure retry = { count: 5, interval: 3000 }count is the maximum number of attempts (not the number of retries — the first attempt counts). interval is the wait between attempts in milliseconds. The default if you don't configure it is count: 3, interval: 1000.
Configure retry before the step that uses it. Retry configuration applies to the next method call that has retry until. After that call completes (success or failure), retry configuration is reset.
You can configure different retry settings for different polls in the same scenario:
# Fast poll for a quick check — 5 attempts, 1 second apart
* configure retry = { count: 5, interval: 1000 }
Given path 'jobs', jobId, 'status'
And retry until response.state != 'queued'
When method get
# Slow poll for a longer process — 12 attempts, 5 seconds apart
* configure retry = { count: 12, interval: 5000 }
Given path 'reports', reportId
And retry until response.ready == true
When method getretry until with a complex condition
The condition after retry until is a JavaScript expression. Use && and || for compound conditions:
# Retry until status is not 'pending' (can be 'completed' or 'failed')
And retry until response.status != 'pending'
When method get
Then status 200
And match response.status == 'completed'After the retry succeeds, normal assertions apply. The retry until condition is just the exit gate — the match that follows confirms the exact expected value.
When to use polling vs waiting
Polling is the right choice when:
- The API returns
202 Acceptedand the actual processing is asynchronous - Data propagation takes time (writes that replicate to read replicas)
- A job or report takes a variable amount of time to complete
- You're checking service health during a deployment
Polling is the wrong choice when:
- The API is synchronous — if it returns
201, the resource is there - You're using polling to mask a race condition that should be fixed in the service
- The interval is so long the test suite becomes impractically slow
A 10-second poll with 12 attempts means a test can wait up to 2 minutes. Think carefully about total suite runtime when configuring retries for multiple scenarios.
The retry loop, step by step
Step 1 of 6
GET request sent
Karate sends the configured GET request. This is attempt 1 of the configured count.
Comparing with other tools
Rest Assured has no built-in retry mechanism — you need a third-party library (Awaitility) or a custom Java loop. Playwright uses waitForResponse() which works at the browser network layer. Cypress uses cy.intercept() and cy.wait(). Karate's retry until is simpler than all of these: one configuration line and one qualifier on the method step.
# Karate — three lines including config
* configure retry = { count: 10, interval: 2000 }
And retry until response.status == 'completed'
When method getThe equivalent in Rest Assured requires an Awaitility dependency, a ConditionFactory setup, and a polling lambda — typically 15–20 lines of Java.
⚠️ Common mistakes
- Putting
retry untilaftermethodinstead of before it. Theretry untilqualifier must go before themethodstep — it tells Karate how to treat the nextmethodcall.When method get / And retry until ...is the wrong order and will throw a parsing error. The correct order is alwaysAnd retry until <condition> / When method get. - Setting a retry count that makes the test suite impractically slow. Ten retries with a 5-second interval means a single scenario can take 50 seconds on the unhappy path. If you have 20 such scenarios, the suite can run for 16 minutes on failure. Set intervals appropriate to the expected processing time — if the API typically processes in 3 seconds,
count: 5, interval: 1000gives 5 seconds of headroom without wasting time. - Not asserting the final state after
retry until. Theretry untilcondition exits the loop when true, but it doesn't count as an assertion in the HTML report. Always follow the retry block with an explicitmatchon the field you polled — it documents what you expected, confirms the value in the report, and catches the edge case where the condition was true for a transitional reason.
🎯 Practice task
Implement a polling scenario against a simulated async API. 30–40 minutes.
- JSONPlaceholder is synchronous, so simulate an async scenario locally: write a
Scenariothat calls/todos/1(which has acompletedboolean field). Write* configure retry = { count: 3, interval: 500 }, thenAnd retry until response.completed == true, thenWhen method get. Sincetodos/1hascompleted: false, the retry will exhaust — read the failure message carefully. This demonstrates the "condition never met" failure path. - Change the scenario to poll
/todos/1withAnd retry until response.id == 1. Sinceidis always1, the first attempt succeeds. Confirm the retry loop exits after one attempt. - Write a second scenario with
And retry until response.completed == false. This succeeds immediately because/todos/1hascompleted: false. Add amatch response.completed == falseafter themethod getand confirm it passes. - Configure different retry settings for two sequential polls in the same scenario: first a fast poll (count 3, interval 500ms), then a slow poll (count 2, interval 1000ms). Observe in the console that Karate sends multiple requests.
- Read the Awaitility docs (briefly — just the README). Note how much Java boilerplate is required for the equivalent of
retry until. Appreciate the Karate alternative. - Stretch: write a utility feature
common/wait-for-status.featurethat acceptsresourcePath,targetStatus,retryCount, andretryIntervalas arguments. It configures retry with the passed values and pollsresourcePathuntilresponse.status == targetStatus. Call it from a main feature withcall read(...). This is the reusable polling pattern used in production Karate suites.
Next lesson: parallel execution — running feature files across multiple threads with one line of configuration.