Testing a JSON-based POST endpoint is one of the most common tasks in performance testing. In Gatling, this means building a request that sends a valid JSON payload, uses the correct headers, and verifies that the server responds as expected under load. A well-written Gatling POST test is not only useful for measuring response times; it also helps confirm whether an API behaves reliably when many users submit data at the same time.
TLDR: A Gatling POST call with a JSON request body requires three main elements: the endpoint URL, the JSON payload, and the correct HTTP headers. The most important header is usually Content-Type: application/json, because it tells the server how to interpret the body. Gatling lets you define JSON directly in the test, load it from a file, or generate it dynamically using session variables and feeders. A serious test should also include checks, realistic data, and meaningful load scenarios.
Understanding the Basic Structure of a Gatling POST Call
Gatling is a performance testing framework designed to simulate users interacting with applications and APIs. When testing a POST endpoint, the goal is typically to send data to the server, such as a login request, an order submission, a user registration form, or a payment initiation request.
A basic Gatling HTTP POST request has the following parts:
- HTTP method:
POST - Endpoint path: the API route being tested
- Headers: especially
Content-Typeand sometimes authorization headers - Body: the JSON payload sent to the server
- Checks: assertions that validate the response
In practice, the request must look like something a real client would send. Performance tests that use incomplete or unrealistic requests can produce misleading results, even if the test technically runs without errors.
A Simple POST Request With an Inline JSON Body
The most direct way to send JSON in Gatling is to define the body inside the test using StringBody. This is useful for simple examples, small payloads, or when the JSON does not need to change between virtual users.
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
val scn = scenario("Create user scenario")
.exec(
http("Create user")
.post("/users")
.body(StringBody("""
{
"name": "John Smith",
"email": "john.smith@example.com",
"role": "customer"
}
""")).asJson
.check(status.is(201))
)
setUp(
scn.inject(atOnceUsers(1))
).protocols(httpProtocol)
In this example, .post("/users") defines the endpoint, while StringBody contains the JSON request body. The .asJson call is important because it marks the body as JSON and sets the proper content type when needed. The response is validated with .check(status.is(201)), which confirms that the API created the user successfully.
Although this example is small, the same principles apply to more complicated test cases. The request must be structurally valid, the server must receive the expected headers, and the test should verify the response rather than simply sending traffic blindly.
Why Headers Matter
Headers are often the reason a POST request fails during performance testing. An API may reject a request if it does not know how to parse the body, or if the authentication information is missing. For JSON APIs, the most common header is:
Content-Type: application/json
This tells the server that the request body should be interpreted as JSON. Another common header is:
Accept: application/json
This indicates that the client expects a JSON response. For protected APIs, you may also need an authorization header:
.header("Authorization", "Bearer your_token_here")
In a serious test, tokens should usually not be hardcoded. They may be generated through a login request, loaded from test data, or passed through environment configuration. Hardcoding sensitive values is risky and makes tests harder to maintain.
Using Dynamic Values in the JSON Body
Real users do not usually submit identical data. If every virtual user sends the same email address, username, or transaction ID, the API may respond with duplicate errors. Gatling supports dynamic request bodies through session variables and feeders.
A feeder is a data source that provides values to virtual users. It can read from CSV files, JSON files, databases, or custom logic. A common example is a CSV file containing user records:
name,email,role
Alice Brown,alice1@example.com,customer
Robert Green,robert1@example.com,admin
The feeder can then be connected to the scenario:
val userFeeder = csv("users.csv").circular
val scn = scenario("Create users with feeder")
.feed(userFeeder)
.exec(
http("Create dynamic user")
.post("/users")
.body(StringBody("""
{
"name": "#{name}",
"email": "#{email}",
"role": "#{role}"
}
""")).asJson
.check(status.in(200, 201))
)
Here, #{name}, #{email}, and #{role} are replaced at runtime using values from the feeder. This makes the test more realistic and reduces the risk of false failures caused by duplicate data.
Loading JSON From an External File
For larger request bodies, placing JSON directly inside the test code can become difficult to read and maintain. Gatling allows you to load the body from a file using RawFileBody. This is a cleaner approach when testing complex payloads such as orders, insurance applications, nested customer profiles, or financial transactions.
http("Submit order")
.post("/orders")
.body(RawFileBody("bodies/order.json")).asJson
.check(status.is(201))
The file is usually placed under the Gatling resources directory. This keeps the simulation code focused on test flow while the JSON file remains separate and easier to edit. If the file contains placeholders such as #{customerId}, Gatling can still resolve them from the session, provided the body type and configuration support expression evaluation.
External files are especially useful when the same payload is reused across multiple scenarios. They also make it easier for testers, developers, and analysts to review the request body without reading the entire simulation code.
Adding Response Checks
A POST request should not be considered successful only because Gatling was able to send it. The server response must be checked. At a minimum, the status code should be validated. In many cases, the response body should also be inspected.
.check(status.is(201))
.check(jsonPath("$.id").exists)
.check(jsonPath("$.email").is("#{email}"))
These checks confirm that the server returned the expected status, produced an identifier, and echoed the correct email address. More advanced tests may save values from a response and use them in later requests. For example, after creating a user, the test may extract the user ID and use it to update or delete that user.
This type of request chaining reflects real application behavior more accurately. It also helps identify failures that would not be visible from status codes alone.
Common Mistakes to Avoid
When testing POST calls with JSON bodies in Gatling, several mistakes appear frequently:
- Missing content type: Without
application/json, the server may reject or misinterpret the request. - Invalid JSON syntax: A missing comma, quote, or brace can cause immediate request failure.
- Hardcoded duplicate data: Reusing the same email or ID can trigger business validation errors.
- No response validation: A test that does not check responses may hide functional failures.
- Unrealistic load patterns: Sending all users at once may not represent real production traffic.
These issues can make results unreliable. A performance test should be treated as a controlled technical experiment, not just a traffic generator.
Designing a Meaningful Load Scenario
After the POST request itself is correct, the next step is choosing a load model. Gatling supports several injection profiles, such as sending users at once, ramping users gradually, or maintaining a constant arrival rate.
setUp(
scn.inject(
rampUsers(100).during(60)
)
).protocols(httpProtocol)
This example gradually starts 100 users over 60 seconds. A ramp-up pattern is often more realistic than launching all users immediately. For production-like testing, the load model should be based on real traffic data whenever possible, including peak usage periods, average request rates, and user behavior patterns.
It is also important to monitor the system under test during execution. Gatling reports response times, percentiles, failed requests, and throughput, but server metrics such as CPU, memory, database performance, and queue depth are equally important for understanding bottlenecks.
Conclusion
A Gatling test for a POST call with a JSON request body is straightforward in concept, but it must be implemented carefully to produce reliable results. The request needs valid JSON, correct headers, realistic data, and meaningful checks. For small payloads, StringBody is simple and effective; for larger payloads, RawFileBody keeps tests cleaner and easier to maintain.
The most trustworthy performance tests resemble real client behavior. They use dynamic data, validate responses, and apply load patterns that reflect actual usage. When these principles are followed, Gatling becomes a strong tool not only for measuring API speed, but also for identifying reliability issues before they affect users.