EXTENSIONS
Performance Testing
Reuse your Karate functional tests as Gatling performance tests. Validate API correctness under load with full response assertions, not just status codes. Load models are written in Java; all test logic stays in Karate.
On this page:
- Quick Start - Maven and Gradle setup
- Java DSL - Simulation example
- karateProtocol() - URL pattern configuration
- nameResolver - Custom request naming for GraphQL/SOAP
- karateFeature() - Execute features as load tests
- Tag Selectors - Select specific scenarios
- Data Flow - Variables, sessions, and feeders
- Think Time - Realistic user pauses
- Custom Java Code - Non-HTTP performance testing
- Configuration - Thread pools, logging, profiles
- Troubleshooting - Common issues and solutions
Quick Start
Maven Setup
<properties>
<karate.version>2.0.0</karate.version>
<gatling.plugin.version>4.21.6</gatling.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.karatelabs</groupId>
<artifactId>karate-gatling</artifactId>
<version>${karate.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling.plugin.version}</version>
<configuration>
<simulationsFolder>src/test/java</simulationsFolder>
<includes>
<include>perf.UsersSimulation</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
Run performance tests:
# Compile and run
mvn clean test-compile gatling:test
# Run specific simulation
mvn clean test-compile gatling:test -Dgatling.simulationClass=perf.UsersSimulation
Gradle Setup
dependencies {
testImplementation "io.karatelabs:karate-gatling:2.0.0"
}
task gatlingRun(type: JavaExec) {
classpath = sourceSets.test.runtimeClasspath
mainClass = 'io.gatling.app.Gatling'
args = ['-s', 'perf.UsersSimulation', '-rf', 'build/reports/gatling']
}
Ensure all *.feature files are copied to the resources folder when you build.
Watch the Karate Gatling webinar for a complete walkthrough of performance testing with Karate.
Java DSL
A complete simulation:
package perf;
import io.karatelabs.gatling.KarateProtocolBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.karatelabs.gatling.KarateDsl.*;
public class UsersSimulation extends Simulation {
public UsersSimulation() {
KarateProtocolBuilder protocol = karateProtocol(
uri("/users/{id}").nil(),
uri("/users").pauseFor(method("get", 15), method("post", 25))
);
protocol.runner.karateEnv("perf");
ScenarioBuilder getUsers = scenario("get users")
.exec(karateFeature("classpath:perf/get-users.feature"));
ScenarioBuilder createUser = scenario("create user")
.exec(karateFeature("classpath:perf/create-user.feature"));
setUp(
getUsers.injectOpen(rampUsers(10).during(5)).protocols(protocol),
createUser.injectOpen(rampUsers(5).during(5)).protocols(protocol)
);
}
}
The feature file contains your test logic with full assertions:
Feature: Get users performance test
Scenario: Get all users
Given url 'https://jsonplaceholder.typicode.com'
And path 'users'
When method get
Then status 200
And match response == '#[10]'
And match each response contains { id: '#number', name: '#string', email: '#string' }
karateProtocol()
The protocol configuration is required because Karate makes HTTP requests while Gatling manages timing and threads. Declare URL patterns so requests aggregate correctly in Gatling reports.
KarateProtocolBuilder protocol = karateProtocol(
uri("/users/{id}").nil(), // No pause for this pattern
uri("/users").pauseFor(method("get", 15), method("post", 25)), // 15ms for GET, 25ms for POST
uri("/orders/{orderId}/items/{itemId}").nil() // Path parameters use {name}
);
Without this configuration, each unique URL (e.g., /users/1, /users/2) would appear as a separate entry in reports instead of aggregating under /users/{id}.
pauseFor()
Set pause times (in milliseconds) per URL pattern and HTTP method. The pause is applied before the matching request. Use nil() for zero pause on all methods.
Set pauses to 0 unless you need to artificially limit requests per second. For realistic user think time, use karate.pause() within your feature files instead.
nameResolver
For GraphQL and SOAP APIs where the URI stays constant but the payload changes, use nameResolver to customize how requests are named in reports:
protocol.nameResolver((req, vars) -> req.getHeader("karate-name"));
In your feature file, set a custom header to control the report name:
Feature: GraphQL performance test
Scenario: Get user by ID
Given url graphqlUrl
And header karate-name = 'graphql-getUser'
And request { query: '{ user(id: 1) { name email } }' }
When method post
Then status 200
If nameResolver returns null, Karate falls back to the default URL-based naming.
runner Configuration
Access Runner.Builder methods for custom configuration:
// Set Karate environment (uses karate-config-perf.js)
protocol.runner.karateEnv("perf");
// Set config directory
protocol.runner.configDir("src/test/resources");
// Set system properties
protocol.runner.systemProperty("api.baseUrl", "https://perf-api.example.com");
Setting karateEnv("perf") loads karate-config-perf.js in addition to karate-config.js. Alternatively, pass -Dkarate.env=perf on the command line.
karateFeature()
Execute entire Karate features as performance test flows:
ScenarioBuilder scenario = scenario("user flow")
.exec(karateFeature("classpath:perf/user-flow.feature"));
Multiple features can run concurrently with different load profiles:
setUp(
browsing.injectOpen(constantUsersPerSec(7).during(300)).protocols(protocol),
purchasing.injectOpen(constantUsersPerSec(2).during(300)).protocols(protocol),
admin.injectOpen(constantUsersPerSec(1).during(300)).protocols(protocol)
);
Silent Execution
For warm-up phases that should not count toward statistics:
ScenarioBuilder warmup = scenario("warmup")
.exec(karateFeature("classpath:perf/warmup.feature").silent());
Tag Selectors
Select specific scenarios from a feature file by passing tag expressions as positional arguments after the path:
// Run scenario with a specific tag
karateFeature("classpath:perf/users.feature", "@smoke")
// Value tag (e.g. select by @name=delete)
karateFeature("classpath:perf/users.feature", "@name=delete")
// OR logic with comma
karateFeature("classpath:perf/users.feature", "@smoke,@critical")
// AND logic with separate arguments
karateFeature("classpath:perf/users.feature", "@smoke", "@fast")
// Exclude tags
karateFeature("classpath:perf/users.feature", "~@slow")
This allows reusing functional test scenarios for performance testing without modification.
Data Flow
Gatling Session Access
Access Gatling session data in Karate via the __gatling namespace:
Feature: Access Gatling data
Scenario: Use Gatling user ID
* print 'Gatling userId:', __gatling.userId
Given url baseUrl
And path 'users', __gatling.userId
When method get
Then status 200
Karate Variables in Gatling
Variables created in a karateFeature() execution are added to the Gatling session:
ScenarioBuilder create = scenario("create")
.exec(karateFeature("classpath:perf/create-user.feature"))
.exec(session -> {
System.out.println("Created user ID: " + session.getString("userId"));
return session;
});
karateSet()
Inject Gatling session data into Karate variables:
ScenarioBuilder scenario = scenario("with data")
.exec(karateSet("username", session -> "user_" + session.userId()))
.exec(karateFeature("classpath:perf/user-actions.feature"));
Feeders
Use Gatling feeders to supply test data:
import java.util.*;
public class TestData {
private static final AtomicInteger counter = new AtomicInteger();
private static final List<String> names = Arrays.asList("Alice", "Bob", "Carol", "Dave");
public static String getNextName() {
return names.get(counter.getAndIncrement() % names.size());
}
}
Iterator<Map<String, Object>> feeder = Stream.generate(() -> {
Map<String, Object> row = new HashMap<>();
row.put("userName", TestData.getNextName());
row.put("timestamp", System.currentTimeMillis());
return row;
}).iterator();
ScenarioBuilder scenario = scenario("with feeder")
.feed(feeder)
.exec(karateFeature("classpath:perf/create-user.feature"));
Access feeder values in your feature:
Feature: Create user with feeder data
Scenario: Create user
* print 'Creating user:', __gatling.userName
Given url baseUrl
And path 'users'
And request { name: '#(__gatling.userName)' }
When method post
Then status 201
Chaining Scenarios
Variables flow between features within the same Gatling scenario:
ScenarioBuilder flow = scenario("user flow")
.exec(karateFeature("classpath:perf/create-user.feature")) // Creates userId
.exec(karateFeature("classpath:perf/update-user.feature")) // Uses userId from above
.exec(karateFeature("classpath:perf/delete-user.feature")); // Uses userId from above
karate.callSingle()
Run setup code once across all threads (e.g., authentication):
function fn() {
var config = { baseUrl: 'https://api.example.com' };
// Runs once globally, even with parallel threads
var auth = karate.callSingle('classpath:auth/get-token.feature');
config.authToken = auth.token;
return config;
}
callSingle and callonce lock all threads during execution, which may impact Gatling performance. For high-throughput tests, prefer using feeders for test data.
Detecting Gatling at Runtime
Write features that work both in functional tests and performance tests:
Feature: Dual-purpose test
Scenario: Get user
# Use feeder value if running in Gatling, otherwise use default
* def userName = karate.get('__gatling.userName', 'TestUser')
Given url baseUrl
And path 'users'
And param name = userName
When method get
Then status 200
Think Time
Use karate.pause() for non-blocking pauses that work correctly with Gatling:
Feature: Realistic user flow
Scenario: Shopping journey
# Browse products
Given url baseUrl
And path 'products'
When method get
Then status 200
# User thinks for 2 seconds
* karate.pause(2000)
# View product details
Given path 'products', response[0].id
When method get
Then status 200
# User thinks for 3 seconds before purchase
* karate.pause(3000)
# Add to cart
Given path 'cart'
And request { productId: '#(response.id)', quantity: 1 }
When method post
Then status 201
Thread.sleep() blocks threads and interferes with Gatling's non-blocking architecture. Always use karate.pause() for think time in performance tests.
By default, karate.pause() only works during Gatling execution. To enable it in normal test runs:
* configure pauseIfNotPerf = true
configure localAddress
Bind HTTP requests to a specific local IP address to avoid rate limiting:
Feature: Distributed load test
Scenario: Request from specific IP
* configure localAddress = '192.168.1.100'
Given url baseUrl
And path 'users'
When method get
Then status 200
For round-robin IP selection:
* if (__gatling) karate.configure('localAddress', IpPool.getNextIp())
Custom Java Code
Test non-HTTP protocols (gRPC, databases, message queues) with full Gatling reporting using PerfContext:
package perf;
import io.karatelabs.core.PerfContext;
import java.util.Collections;
import java.util.Map;
public class CustomProtocol {
public static Map<String, Object> callDatabase(Map<String, Object> request, PerfContext context) {
long startTime = System.currentTimeMillis();
// Your custom code here (database call, gRPC, etc.)
String query = (String) request.get("query");
// ... execute query ...
long endTime = System.currentTimeMillis();
// Report to Gatling
context.capturePerfEvent("db-query-" + query.hashCode(), startTime, endTime);
return Collections.singletonMap("success", true);
}
}
Call from your feature file:
Feature: Database performance test
Background:
* def CustomProtocol = Java.type('perf.CustomProtocol')
Scenario: Query performance
* def request = { query: 'SELECT * FROM users WHERE active = true' }
* def result = CustomProtocol.callDatabase(request, karate)
* match result == { success: true }
The karate object implements PerfContext, so pass it directly to your Java methods. Test failures are automatically linked to the captured performance event.
Custom Java integration enables performance testing for:
- Database queries
- gRPC services
- Message queues (Kafka, RabbitMQ)
- Proprietary protocols
- Any Java-callable code
Reporting
URI Pattern Matching
Requests are automatically grouped by URI pattern in Gatling reports:
---- Requests ------------------------------------------------------------------
> Global (OK=12 KO=2 )
> POST /cats (OK=5 KO=2 )
> GET /cats/{id} (OK=5 KO=0 )
> custom-rpc (OK=2 KO=0 )
Configure patterns in karateProtocol() to group requests like /cats/1, /cats/2 under GET /cats/{id}.
Gatling Group Support
Wrap one or more karateFeature() calls in Gatling's group() DSL to get a nested section in the report with its own aggregated counts, response times, and error rates:
ScenarioBuilder flow = scenario("user flow")
.group("Search").on(
exec(karateFeature("classpath:perf/search.feature"))
)
.group("Checkout").on(
exec(karateFeature("classpath:perf/checkout.feature"))
);
Requests fired from inside a group are aggregated under that group in the HTML report:
---- Requests ------------------------------------------------------------------
> Global (OK=40 KO=0)
> Search / GET /products/{id} (OK=10 KO=0)
> Search / GET /search (OK=10 KO=0)
> Checkout / POST /cart (OK=10 KO=0)
> Checkout / POST /orders (OK=10 KO=0)
Group-scoped Gatling assertions work as expected:
.assertions(
details("Search", "GET /search").failedRequests().percent().is(0.0),
details("Checkout", "POST /orders").responseTime().mean().lt(500)
)
Silent Mode
Skip metrics reporting during warm-up iterations:
ScenarioBuilder warmUp = scenario("warm up")
.exec(karateFeature("classpath:perf/get-users.feature").silent());
Fail-Fast Behavior
Karate aborts immediately on the first assertion failure during load tests. Partial results (successful requests before the failure) are still reported to Gatling with the correct timing.
Session Variable Chaining
Variables flow between features in a Gatling scenario via __karate and __gatling maps:
# create-user.feature — stores result in session
* def userId = response.id
# get-user.feature — reads from previous feature's result
* def userId = __karate.userId
* path 'users', userId
Configuration
Maven Profile for Isolation
Keep Gatling dependencies separate to avoid conflicts with your main test framework:
<profiles>
<profile>
<id>gatling</id>
<dependencies>
<dependency>
<groupId>io.karatelabs</groupId>
<artifactId>karate-gatling</artifactId>
<version>${karate.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling.plugin.version}</version>
<configuration>
<simulationsFolder>src/test/java</simulationsFolder>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Run with the profile:
mvn clean test -P gatling
Thread Pool Configuration
By default, Karate-Gatling supports approximately 30 RPS. For higher throughput, create gatling-akka.conf in src/test/resources:
akka {
actor {
default-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 100
}
throughput = 1
}
}
}
Adjust fixed-pool-size based on your target RPS and available system resources.
Logging
For performance tests, reduce logging overhead in logback-test.xml:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<immediateFlush>false</immediateFlush>
</appender>
<logger name="io.karatelabs" level="WARN"/>
<root level="WARN">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
You can also suppress logging in karate-config-perf.js:
function fn() {
// Drop INFO captures (HTTP bodies, print, karate.log) — load tests don't
// need every request/response in the report buffer
karate.configure('logging', { report: 'warn', console: 'warn' });
return { baseUrl: 'https://api.example.com' };
}
Limitations
| Limitation | Details |
|---|---|
| Throttle not supported | Gatling's throttle syntax is not available. Use pauseFor() or karate.pause() instead. |
| Default RPS ~30 | Increase thread pool size for higher throughput. See Thread Pool Configuration. |
| Non-blocking pause required | Never use Thread.sleep(). Use karate.pause() for think time. |
Distributed Testing
For large-scale load tests across multiple machines or Docker containers, see the Distributed Testing Wiki.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Low RPS | Default thread pool too small | Increase fixed-pool-size in gatling-akka.conf |
| Test freezing | Long response times blocking threads | Increase thread pool, check readTimeout |
| OutOfMemoryError | Too much test data in memory | Optimize data usage, increase heap size |
| Connection timeouts | Network issues or server overload | Increase connectTimeout, check target server |
| Requests not aggregating | Missing karateProtocol() patterns | Add URL patterns with path parameters |
| Variables not flowing | Scenario isolation | Use chaining within same Gatling scenario |
Resources
- Demo Video - Complete Karate Gatling walkthrough
- Contract Testing with Karate - API mocks and test doubles
- karate-gatling tests - Reference simulations in the karate repo
Next Steps
- Test Doubles - Create mock services for isolated performance testing
- Configuration - Environment-specific settings
- Parallel Execution - Maximize test throughput