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.
For the rationale behind reusing one suite for both correctness and load, rather than maintaining separate scripts, see API performance testing.
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.
logReplay()
The protocol also decides how much Karate output survives a failure under load — logReplay(), logReplayLevel() and logReplayLimit(). See Replaying Karate output when a feature fails.
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 variables are exposed in Karate under the __gatling map, and variables carried over from a previous karateFeature() in the same scenario under the __karate map. Access is always prefixed — __gatling.userId, __karate.orderId.
This is a breaking change from v1, which also flattened these to top-level (so a bare userId worked too). v2 namespaces them strictly to avoid collisions with Karate built-ins (a Gatling attribute named request or response would otherwise shadow them) and the hazards of placing JS-backed objects directly into the Gatling session.
Because they arrive as call arguments, __gatling variables are not available while karate-config.js runs (config evaluates first). Read them inside a scenario instead — use karate.get('__gatling.x', default) for a value that may be absent in non-Gatling runs.
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 carried in the Gatling session under the __karate map, ready for the next feature in the same scenario to read as __karate.<name>:
ScenarioBuilder create = scenario("create")
.exec(karateFeature("classpath:perf/create-user.feature")) // sets userId
.exec(karateFeature("classpath:perf/use-user.feature")); // reads __karate.userId
* def userId = __karate.userId
* path 'users', userId
v1 also flattened a feature's result variables to top-level Gatling session attributes, so Java-side code could call session.getString("userId") directly. v2 keeps them only under the __karate attribute (a map). To inspect a result on the Java side, read that map rather than a flat attribute.
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}.
Failure messages in the report
A KO carries the failed step's location, the step itself, and the reason — so the errors table says both where and why:
---- Errors --------------------------------------------------------------------
> /project/target/test-classes/perf/checkout.feature: 3 (100%)
26 And match response.quotation != "#object" - match failed: NOT_EQUALS
Only the first line of the reason appears here, on purpose: Gatling groups the errors table by this exact string, so a full match diff — which embeds the actual values, different for every virtual user — would turn every KO into its own row. The complete diff goes to the log instead, where the same file:line and reason lead the failure entry, letting you line the two up. See Logging.
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
Under Gatling, Karate assumes you want no HTML report and no logging except on errors. There is no HTML report in this lane, so the per-step log — the HTTP request/response blocks and print output that a normal run collects into StepResult.log for the report — is not collected at all: nothing would read it, and building it means re-parsing and pretty-printing every response body. Everything else below is about the console, which is yours to configure.
This is capture, not logging. print and karate.log() still reach SLF4J at whatever level your Logback config allows, and so do the HTTP one-liners on karate.http.
Two things turn the capture back on:
logReplay— it reads exactly this buffer, so asking for a replay switches capture on for you. You do not have to do anything else.protocol.runner.captureStepLogs(true)— the explicit opt-in, if you consume theFeatureResultyourself.
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 quieten the terminal from karate-config-perf.js instead — but pick one lane, not both:
function fn() {
// 'console' overwrites the Logback level of every karate.* category every
// time this config is evaluated, so it cancels the per-category config above.
// Leave it out if logback-test.xml is doing the work:
// karate.configure('logging', { console: 'warn' });
return { baseUrl: 'https://api.example.com' };
}
Setting logging.console wins over logback-test.xml for the whole karate tree, run-wide — nothing restores your XML levels; leaving it unset keeps your XML in charge. See logging.console and your logback.xml.
report: 'warn' if you use log replayThe report threshold still filters what is captured, so a config that drops INFO leaves the replay with nothing to show. See Replaying Karate output when a feature fails.
A focused log file: only your prints, HTTP, and failures
During load runs the most useful file is usually just what you explicitly print, the HTTP traffic, and which features failed. Karate routes each to its own SLF4J category, so a logback-test.xml can keep exactly those and drop everything else — again, only with logging.console unset:
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>target/karate.log</file>
<encoder><pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern></encoder>
</appender>
<root level="OFF">
<appender-ref ref="FILE"/>
</root>
<!-- your print / karate.log output -->
<logger name="karate.scenario" level="INFO" additivity="false"><appender-ref ref="FILE"/></logger>
<!-- HTTP request/response (TRACE = full body) -->
<logger name="karate.http" level="TRACE" additivity="false"><appender-ref ref="FILE"/></logger>
<!-- one ERROR per failed feature, including the failure detail -->
<logger name="io.karatelabs.gatling.KarateExecutor" level="ERROR" additivity="false"><appender-ref ref="FILE"/></logger>
<!-- replayed output of the features around a failure (opt-in, see below) -->
<logger name="io.karatelabs.gatling.LogReplayer" level="ERROR" additivity="false"><appender-ref ref="FILE"/></logger>
</configuration>
When a feature fails under load, io.karatelabs.gatling.KarateExecutor logs a single ERROR that leads with the location and the reason, then shows the detail — so you can diagnose a load-run failure without an HTML report:
Feature failed: classpath:perf/checkout.feature:26 - match failed: NOT_EQUALS
/project/target/test-classes/perf/checkout.feature:26 And match response.quotation != "#object"
# the quotation should not be an object
match failed: NOT_EQUALS
$ | actual and expected are equal
...
The first line carries the same file:line and reason as the KO message in the report, so the two line up; the full diff follows underneath, where it has room. See Logging → SLF4J categories for the full category list.
Replaying Karate output when a feature fails
A load run is usually pinned to a high Logback level — often ERROR, to stay inside a CI log-size limit — so the per-step Karate output never reaches the log. When a Gatling scenario calls several features and the third one fails, nothing survives to say what the first two did, and the failing feature's own HTTP traffic is gone too.
Turning replay on makes Karate capture that output regardless of the Logback level — capture is otherwise off in this lane (above) — so it can be held and released only for the runs that actually failed:
KarateProtocolBuilder protocol = karateProtocol(
uri("/cats/{id}").nil()
)
.logReplay("all") // "off" (default) | "failed" | "all"
.logReplayLevel("error") // level the replay is logged at, default error
.logReplayLimit(5); // features held per virtual user, default 5
| Mode | What gets logged when a feature fails |
|---|---|
OFF (default) | Nothing beyond the failure entry. Costs nothing — the per-step output is not even captured, let alone rendered or held, and the results of called features are dropped as usual. |
FAILED | The failing feature's own output — its HTTP traffic and print lines. |
ALL | The features that already passed for that virtual user too, oldest first, then the failing one. |
The replay carries everything the feature captured — the HTTP request/response blocks and your print / karate.log() / karate.logger.info() lines — and arrives as one log event on io.karatelabs.gatling.LogReplayer, framed per feature and headed per scenario, as plain text (no ANSI):
>>> karate: classpath:perf/login.feature [passed]
--- scenario [1:5] login [passed]
request:
1 > POST http://api.example.com/login
...
[print] logged in as user-42
<<< karate: classpath:perf/login.feature
>>> karate: classpath:perf/checkout.feature [failed]
--- scenario [1:8] add to cart [passed]
...
>>> call: features/cart-add.feature [passed]
--- scenario [1:3] add [passed]
request:
1 > POST http://api.example.com/cart
...
<<< call: features/cart-add.feature
--- scenario [2:21] pay [failed]
...
<<< karate: classpath:perf/checkout.feature
A called feature appears in place under the step that called it, indented two spaces per call depth — however it was called: a call step, karate.call(), or a helper defined in karate-config.js. A call loop appends #0, #1 … to the path.
The [section:line] in a scenario header is the same reference the HTML report uses ([1.3:12] for the third example of an outline), so a feature with several scenarios reads unambiguously. A scenario that captured nothing gets no header.
logReplayLevel defaults to error so the replay survives the quiet Logback config a load run usually has — set it lower if you route the replay to its own appender. For the typed form, logReplay() also takes the KarateLogReplay enum: .logReplay(KarateLogReplay.ALL), importing io.karatelabs.gatling.KarateLogReplay.
ALL onIt holds log text in memory for every virtual user until it is replayed or dropped — raise logReplayLimit with the run's concurrency in mind.
Gatling gives an action no iteration boundary to hook, and the session persists across repeat / during. The retained window is therefore the last N Karate calls for this virtual user, cleared after every replay — which is the current iteration in the common case. When the cap drops entries, the replay says how many rather than reading as the whole story.
Switching replay on is enough to get the capture it replays: it turns on the per-step capture and also keeps the results of called features, so the replay can descend into them. Both last only for the one execution being replayed — the call results and step logs are released as soon as the replay has rendered — so the cost is bounded to that execution. But what can be replayed is still bounded by the report threshold (logging.report), not the Logback level. HTTP blocks and print enter the report buffer at INFO, so a karate-config-perf.js that raises report above info — 'warn', say — filters them out and usually leaves the replay empty; when that happens, the replay logs a line saying the feature captured no output rather than going silent. The default is debug, which already captures everything including karate.logger.debug() output, so the threshold is only ever in your way if your own config raised it. See Two thresholds: report vs console.
Under Gatling (no interactive TTY) Karate disables colour automatically, so the log file above contains no ANSI escape codes — no %replace(...) regex needed in your pattern. See Logging → Colour in console vs log files.
Prints only when something fails
The common load-run shape: nothing on the console during a clean run, and the full Karate output — HTTP blocks and your prints — for the features that failed.
<logger name="karate" level="ERROR"/>
<logger name="karate.scenario" level="ERROR"/>
<logger name="karate.http" level="ERROR"/>
<logger name="karate.runtime" level="ERROR"/>
<logger name="io.karatelabs" level="ERROR"/>
The children are listed explicitly because a Logback logger with its own level ignores its parent's — a karate.http TRACE or karate.scenario INFO line left over from another recipe on this page would otherwise keep that output live.
KarateProtocolBuilder protocol = karateProtocol(uri("/cats/{id}").nil())
.logReplay(KarateLogReplay.FAILED);
print / karate.log() and the HTTP blocks then surface only inside the single replay event, which is emitted on io.karatelabs.gatling.LogReplayer at ERROR (logReplayLevel) — so the ERROR pins above do not silence it. If your config pins an intermediate logger such as io.karatelabs.gatling to OFF, pin io.karatelabs.gatling.LogReplayer to ERROR explicitly. Leave logging.report alone: the default debug is what fills the replay, and raising it above info filters out the prints and HTTP blocks, which enter the capture at INFO, and usually leaves the replay empty. Do not set logging.console — it would overwrite the pins (why).
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 |
__gatling.x / __karate.x is null | Accessed without the namespace prefix, or read during karate-config.js | Use the prefixed form (v2 is prefix-only); read inside a scenario, not in config — karate.get('__gatling.x', default) for optional values |
| Feature failures hard to diagnose | HTML report and console summary are off under Gatling | Enable the io.karatelabs.gatling.KarateExecutor ERROR logger — it logs each failure's reason (step + file:line + assertion message). See Logging |
| A failure has no context — no sign of what the earlier features in the scenario did | A quiet Logback level drops the per-step output of everything that passed | Turn on log replay: .logReplay("all") |
| Log replay produces nothing | Your config sets logging.report above info — e.g. following the old printEnabled advice — so the prints and HTTP blocks, which enter the capture at INFO, are filtered out; or the feature failed before its first request or print | Remove the setting, or set it to info or lower — the default debug already captures. See log replay |
| A called feature's output is missing | An older Karate — the replay did not descend into called features, and their results were released at scenario end | Fixed: the replay now renders called features in place. On 2.1.2 or earlier that output was never carried |
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