Retry and Polling Patterns

8 min read

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 get

retry 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 Accepted and 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 get

The 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 until after method instead of before it. The retry until qualifier must go before the method step — it tells Karate how to treat the next method call. When method get / And retry until ... is the wrong order and will throw a parsing error. The correct order is always And 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: 1000 gives 5 seconds of headroom without wasting time.
  • Not asserting the final state after retry until. The retry until condition exits the loop when true, but it doesn't count as an assertion in the HTML report. Always follow the retry block with an explicit match on 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.

  1. JSONPlaceholder is synchronous, so simulate an async scenario locally: write a Scenario that calls /todos/1 (which has a completed boolean field). Write * configure retry = { count: 3, interval: 500 }, then And retry until response.completed == true, then When method get. Since todos/1 has completed: false, the retry will exhaust — read the failure message carefully. This demonstrates the "condition never met" failure path.
  2. Change the scenario to poll /todos/1 with And retry until response.id == 1. Since id is always 1, the first attempt succeeds. Confirm the retry loop exits after one attempt.
  3. Write a second scenario with And retry until response.completed == false. This succeeds immediately because /todos/1 has completed: false. Add a match response.completed == false after the method get and confirm it passes.
  4. 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.
  5. 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.
  6. Stretch: write a utility feature common/wait-for-status.feature that accepts resourcePath, targetStatus, retryCount, and retryInterval as arguments. It configures retry with the passed values and polls resourcePath until response.status == targetStatus. Call it from a main feature with call 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.

// tip to track lessons you complete and pick up where you left off across devices.