Skip to main content

ADVANCED

Polling and Async Operations

Test asynchronous operations, eventual consistency, and event-driven systems using retry patterns, WebSocket connections, and custom polling logic.

On this page:

Retry Until Pattern

Basic Retry

Use retry until to wait for a condition to become true. The retry until keyword must appear before the method step:

Gherkin
Feature: Basic retry

Scenario: Wait for data to appear
* configure retry = { count: 10, interval: 2000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
And retry until response.id == 1
When method get
Then status 200

Retry Configuration

Configure retry attempts and interval globally or per-scenario:

Gherkin
Feature: Retry configuration

Scenario: Custom retry settings
# Set retry: 5 attempts, 2 second intervals
* configure retry = { count: 5, interval: 2000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'posts', 1
And retry until responseStatus == 200
When method get
Default Retry Settings

Default is 3 attempts with 3000ms (3 second) intervals. Configure via configure retry = { count: 3, interval: 3000 }.

Custom Retry Conditions

Use JavaScript expressions in retry conditions. You can reference response, responseStatus, or responseHeaders:

Gherkin
Feature: Complex retry conditions

Scenario: Retry with status check
* configure retry = { count: 5, interval: 1000 }
Given url 'https://jsonplaceholder.typicode.com'
And path 'users', 1
And retry until responseStatus == 200 && response.id > 0
When method get
Then status 200
* match response.id == 1
Pure JavaScript Only

The retry until expression must be pure JavaScript. Karate match syntax like contains will NOT work. Use JavaScript equivalents instead.

Custom Polling Logic

JavaScript Polling Functions

For complex polling scenarios beyond retry until, use karate.http() in JavaScript functions:

Gherkin
Feature: Custom polling

Background:
* def pollUntilReady =
"""
function(url, maxAttempts, interval) {
for (var i = 0; i < maxAttempts; i++) {
var response = karate.http(url).get();
if (response.status == 200) {
return response.body;
}
java.lang.Thread.sleep(interval);
}
karate.fail('Polling timeout after ' + maxAttempts + ' attempts');
}
"""

Scenario: Use custom polling
* def result = pollUntilReady('https://jsonplaceholder.typicode.com/users/1', 5, 1000)
* match result.id == 1
karate.http()

karate.http(url) returns a convenience HTTP request builder for use within JavaScript. Use .get(), .post(), .put(), or .delete() to execute requests.

Progressive Backoff

Exponential backoff doubles the delay between each retry attempt, reducing load on the server while waiting for eventual consistency:

Gherkin
Feature: Exponential backoff

Background:
* def pollWithBackoff =
"""
function(checkFunc, maxAttempts) {
var delay = 1000;
for (var i = 0; i < maxAttempts; i++) {
var result = checkFunc();
if (result.success) return result.data;
java.lang.Thread.sleep(delay);
delay = delay * 2;
}
karate.fail('Max attempts reached');
}
"""
* def checkStatus =
"""
function() {
var res = karate.http('https://jsonplaceholder.typicode.com/users/1').get();
return { success: res.status == 200, data: res.body };
}
"""

Scenario: Poll with backoff
* def result = pollWithBackoff(checkStatus, 5)
* match result.id == 1

Async Event Handling

Listen and Signal

The listen keyword waits for an event, and karate.signal(data) triggers that event. This pattern is useful for message queues, WebSockets, and custom async callbacks.

Gherkin
Feature: Async event handling

Background:
* def QueueConsumer = Java.type('mock.contract.QueueConsumer')
* def queue = new QueueConsumer(queueName)
* def handler = function(msg) { karate.signal(msg) }
* queue.listen(karate.toJava(handler))

Scenario: Wait for async event
Given url paymentServiceUrl + '/payments'
And request { amount: 5.67, description: 'test one' }
When method post
Then status 200
* def id = response.id

# Wait for message on queue (max 5 seconds)
* listen 5000
* json shipment = listenResult
* match shipment == { paymentId: '#(id)', status: 'shipped' }
Listen Timeout

listen accepts timeout in milliseconds. If no event arrives, listenResult will be null. Always check for null to handle timeouts gracefully.

Signal API Reference

The karate.signal(data) method and listenResult variable work together:

ComponentDescription
karate.signal(data)Trigger event with any data type (JSON, string, number, array)
listenResultMagic variable containing the signaled data
karate.toJava(fn)Wrap JS functions for use in Java callbacks

Handling timeouts:

Gherkin
Feature: Timeout handling

Scenario: Handle listen timeout
* listen 5000
* if (listenResult == null) karate.fail('Timeout waiting for event')

Multiple Concurrent Listeners

When testing workflows that produce multiple events, collect them in an array and signal when all expected events have arrived:

Gherkin
Feature: Multiple async sources

Background:
* def events = []
* def eventHandler = function(event) {
events.push(event);
if (events.length >= 2) {
karate.signal(events);
}
}

Scenario: Wait for multiple events
* def EventBus = Java.type('com.example.EventBus')
* EventBus.subscribe('channel-1', karate.toJava(eventHandler))
* EventBus.subscribe('channel-2', karate.toJava(eventHandler))
* karate.call('trigger-events.feature')

# Wait for both events
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[2]'

Message Queue Integration

Test message queue interactions using the listen/signal pattern:

Gherkin
Feature: Message queue testing

Background:
* def QueueConsumer = Java.type('mock.contract.QueueConsumer')
* def queue = new QueueConsumer('order-queue')
* def messageHandler = function(msg) {
if (msg.type === 'order-processed') {
karate.signal(msg);
}
}
* queue.listen(karate.toJava(messageHandler))

Scenario: Test queue message flow
Given url orderServiceUrl + '/orders'
And request { orderId: 'ORD-123', items: ['item1'] }
When method post
Then status 201

# Wait for message on queue
* listen 5000
* def message = listenResult
* match message.orderId == 'ORD-123'
* match message.type == 'order-processed'
Message Queue Testing

The listen/signal pattern works with any message broker (ActiveMQ, RabbitMQ, Kafka) via custom Java classes that forward messages to karate.signal().

WebSocket Support

Karate has built-in WebSocket support using the async listen pattern.

Basic WebSocket Connection

Connect to a WebSocket endpoint, send a message, and wait for a response using listen:

Gherkin
Feature: WebSocket testing

Scenario: Basic WebSocket communication
* def socket = karate.webSocket(demoBaseUrl + '/websocket')
* socket.send('Billie')
* listen 5000
* match listenResult == 'hello Billie !'
WebSocket URLs

WebSocket examples use demoBaseUrl as a placeholder. Configure your actual WebSocket server URL in karate-config.js.

WebSocket with Handler

A handler function filters messages—return true to complete the listen wait:

Gherkin
Feature: WebSocket with filtering

Scenario: Filter WebSocket messages
* def handler = function(msg) { return msg.startsWith('hello') }
* def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)
* socket.send('Billie')
* listen 5000
* match listenResult == 'hello Billie !'

Without a handler (or null), the first message received completes the listen.

WebSocket Options

The third argument to karate.webSocket() accepts configuration options:

Gherkin
Feature: WebSocket options

Scenario: WebSocket with options
* def wsOptions = { subProtocol: 'chat', headers: { 'Authorization': 'Bearer token' }, maxPayloadSize: 8388608 }
* def ws = karate.webSocket(demoBaseUrl + '/websocket', null, wsOptions)
* ws.send('hello')
* listen 5000
* match listenResult == '#notnull'
OptionDescription
subProtocolWebSocket sub-protocol if server requires it
headersCustom headers (e.g., authentication)
maxPayloadSizeMax message size in bytes (default: ~4MB)
WebSocket Cleanup

WebSocket connections are auto-closed at the end of each Scenario. Use ws.close() for explicit cleanup.

Binary WebSocket Messages

For binary protocols (custom wire formats, file transfer, compressed data), use karate.webSocketBinary() instead of the text-based version:

Gherkin
Feature: Binary WebSocket

Scenario: Binary WebSocket communication
* def ws = karate.webSocketBinary(demoBaseUrl + '/binary')
* def binaryData = read('classpath:test-data.bin')
* ws.send(binaryData)
* listen 5000
* match listenResult == '#notnull'
* assert listenResult.length > 0

WebSocket Connection Management

Send multiple messages on the same connection:

Gherkin
Feature: WebSocket lifecycle

Scenario: Multiple messages
* def ws = karate.webSocket(demoBaseUrl + '/websocket')
* ws.send('first')
* listen 2000
* ws.send('second')
* listen 2000
* ws.close()

Reconnection with retry - Handle flaky WebSocket servers by wrapping connection logic in a retry loop:

Gherkin
Feature: WebSocket reconnection

Background:
* def connectWithRetry =
"""
function(url, maxRetries) {
for (var i = 0; i < maxRetries; i++) {
try {
return karate.webSocket(url);
} catch (e) {
karate.log('Connection failed, attempt:', i + 1);
if (i < maxRetries - 1) java.lang.Thread.sleep(2000);
else throw e;
}
}
}
"""

Scenario: Connect with retry
* def ws = connectWithRetry(demoBaseUrl + '/websocket', 3)
* ws.send('hello')
* listen 5000
* match listenResult == '#notnull'

Advanced Patterns

Event Correlation

Filter events by correlation ID to track related async operations:

Gherkin
Feature: Event correlation

Background:
* def correlationId = java.util.UUID.randomUUID().toString()
* def events = []
* def eventHandler = function(event) {
if (event.correlationId === correlationId) {
events.push(event);
if (events.length >= 3) {
karate.signal(events);
}
}
}

Scenario: Correlate multiple events
* def EventSystem = Java.type('com.example.EventSystem')
* EventSystem.registerHandler(karate.toJava(eventHandler))

Given url workflowServiceUrl + '/async-workflow'
And request { correlationId: '#(correlationId)', steps: ['validate', 'process', 'notify'] }
When method post
Then status 202

# Wait for all 3 correlated events
* listen 15000
* def allEvents = listenResult
* match allEvents == '#[3]'
* match each allEvents[*].correlationId == correlationId

Timeout Strategies

For unreliable services, try with short timeouts first and progressively increase if needed:

Gherkin
Feature: Cascading timeouts

Background:
* def tryWithTimeouts =
"""
function(operation, timeouts) {
for (var i = 0; i < timeouts.length; i++) {
try {
karate.configure('readTimeout', timeouts[i]);
return operation();
} catch (e) {
if (i === timeouts.length - 1) throw e;
}
}
}
"""

Scenario: Cascading timeout strategy
* def callApi = function() {
return karate.http('https://jsonplaceholder.typicode.com/users/1').get();
}
# Try with increasing timeouts: 1s, 5s, 10s
* def result = tryWithTimeouts(callApi, [1000, 5000, 10000])
* match result.status == 200

Combining Polling and Events

Combine HTTP triggers with WebSocket confirmation for complete async workflows:

Gherkin
Feature: Complete async workflow

Background:
* def notificationHandler = function(msg) {
var data = JSON.parse(msg);
return data.type === 'job-completed';
}
* def ws = karate.webSocket(demoBaseUrl + '/notifications', notificationHandler)

Scenario: API trigger with event confirmation
# Trigger job via API
Given url jobServiceUrl + '/jobs'
And request { operation: 'process-data', priority: 'high' }
When method post
Then status 202
* def jobId = response.jobId

# Subscribe and wait for completion
* ws.send({ action: 'subscribe', jobId: jobId })
* listen 30000
* def notification = JSON.parse(listenResult)
* match notification.status == 'completed'

# Verify final state
Given path 'jobs', jobId
When method get
Then status 200
* match response.result == '#present'

This pattern combines HTTP (reliable, stateless) with WebSocket (real-time) for robust async testing.

When to Use Polling and Async

Use retry mechanisms when:

  • Testing services with startup delays or eventual consistency
  • Working with distributed systems that have propagation delays
  • Dealing with rate-limited or throttled APIs that require backoff

Use async event handling when:

  • Testing WebSocket real-time communication
  • Integrating with message queues or event buses
  • Coordinating multiple asynchronous operations across services

Configure appropriate timeouts based on:

  • Development environment: Longer timeouts (30-60 seconds)
  • CI/CD pipelines: Moderate timeouts (10-20 seconds)
  • Production testing: Strict timeouts matching SLAs

Next Steps