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. Write load models in Java or Scala while keeping all test logic in Karate.
On this page:
- Quick Start - Maven and Gradle setup
- Java DSL - Modern Java-based simulations
- Scala DSL - Traditional Scala approach
- 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
Add Gatling support to your existing Karate project:
<properties>
<karate.version>1.5.2</karate.version>
<gatling.plugin.version>4.3.7</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>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.5.6</version>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-Jbackend:GenBCode</arg>
<arg>-Jdelambdafy:method</arg>
<arg>-target:jvm-1.8</arg>
<arg>-deprecation</arg>
<arg>-feature</arg>
<arg>-unchecked</arg>
<arg>-language:implicitConversions</arg>
<arg>-language:postfixOps</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
<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
plugins {
id 'scala'
}
dependencies {
testImplementation "io.karatelabs:karate-gatling:1.5.2"
}
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 (Recommended)
Gatling now supports Java-based simulations, eliminating the need to learn Scala. This is the recommended approach for new projects.
package perf;
import com.intuit.karate.gatling.javaapi.KarateProtocolBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Simulation;
import static io.gatling.javaapi.core.CoreDsl.*;
import static com.intuit.karate.gatling.javaapi.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' }
Scala DSL
For teams already using Scala or maintaining existing Scala simulations:
package perf
import com.intuit.karate.gatling.PreDef._
import io.gatling.core.Predef._
import scala.concurrent.duration._
class UsersSimulation extends Simulation {
val protocol = karateProtocol(
"/users/{id}" -> Nil,
"/users" -> pauseFor("get" -> 15, "post" -> 25)
)
protocol.runner.karateEnv("perf")
val getUsers = scenario("get users")
.exec(karateFeature("classpath:perf/get-users.feature"))
val createUser = scenario("create user")
.exec(karateFeature("classpath:perf/create-user.feature"))
setUp(
getUsers.inject(rampUsers(10) during (5 seconds)).protocols(protocol),
createUser.inject(rampUsers(5) during (5 seconds)).protocols(protocol)
)
}
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() (Java) or Nil (Scala) 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, ctx) -> 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 appending tags:
// Run scenario with @name=delete tag
karateFeature("classpath:perf/users.feature@name=delete")
// Multiple tags as separate arguments (AND logic)
karateFeature("classpath:perf/users.feature", "@name=delete")
// OR logic with comma
karateFeature("classpath:perf/users.feature", "@smoke,@critical")
// AND logic with separate strings
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.
Tags set on protocol.runner.tags() are inherited by all karateFeature() calls. You can add more tags per feature but cannot remove inherited ones.
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());
}
}
val feeder = Iterator.continually(Map(
"userName" -> TestData.getNextName(),
"timestamp" -> System.currentTimeMillis()
))
val 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 com.intuit.karate.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
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>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.5.6</version>
<executions>
<execution>
<goals><goal>testCompile</goal></goals>
</execution>
</executions>
</plugin>
<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="com.intuit.karate" level="WARN"/>
<root level="WARN">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
You can also suppress logging in karate-config-perf.js:
function fn() {
karate.configure('logPrettyRequest', false);
karate.configure('logPrettyResponse', false);
karate.configure('printEnabled', false);
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
- Example Project - Working Maven project with simulations
Next Steps
- Test Doubles - Create mock services for isolated performance testing
- Configuration - Environment-specific settings
- Parallel Execution - Maximize test throughput