Server-Sent Events (SSE) streams are surprisingly difficult to load test effectively because they require maintaining long-lived connections and processing continuous data, which traditional HTTP load testing tools often struggle with.
Let’s see Gatling in action with an SSE stream. Imagine a simple backend that pushes stock price updates every second.
// Example SSE endpoint on the server (conceptual, not Gatling)
// GET /stocks/AAPL/stream
// ... server sends:
// data: {"symbol": "AAPL", "price": 170.50, "timestamp": 1678886400}
//
// data: {"symbol": "AAPL", "price": 170.55, "timestamp": 1678886401}
// ... and so on
Here’s a Gatling simulation that connects to such an endpoint, listens for events, and asserts on received data.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class SseSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080") // Your SSE server base URL
.acceptHeader("text/event-stream") // Crucial: tell the server we expect SSE
val scn = scenario("SSE Stream Test")
.exec(
http("Connect to SSE Stream")
.get("/stocks/AAPL/stream")
.check(status.is(200)) // Ensure the connection is established successfully
.asLongAs(global.responseTime.max.lt(5000), // Keep connection alive as long as response time is < 5s
foreach("event".ofStream, "data") // Process each SSE event received
.on(
exec(session => {
// Log the received event data
println(s"Received SSE Event: ${session("data").as[String]}")
session
})
.check(jsonPath("$.symbol").is("AAPL")) // Example assertion on JSON payload
.check(jsonPath("$.price").ofType[Double].gt(0.0)) // Another assertion
)
)
)
setUp(
scn.inject(
rampUsers(10) during (10.seconds), // Gradually ramp up 10 users
constantUsersPerSec(5) during (30.seconds) // Maintain 5 users/sec for 30 seconds
).protocols(httpProtocol)
)
}
This simulation defines an HTTP protocol that specifically requests text/event-stream. The core of the test is the .asLongAs check. This Gatling construct is key for SSE. It continuously processes incoming data from the established connection. Inside .asLongAs, foreach("event".ofStream, "data") iterates over each distinct SSE message received. You can then apply checks to the data payload, such as validating JSON fields or ensuring numerical values are within expected ranges. The global.responseTime.max.lt(5000) condition is a simple way to keep the connection open as long as the server is responding promptly.
The mental model for testing SSE with Gatling revolves around these points:
- Connection Establishment: The initial
http("Connect to SSE Stream").get(...)is like opening a WebSocket. It needs to succeed (HTTP 200 OK). - Stream Processing: The
asLongAsblock is the heart. It’s not about making separate requests; it’s about listening on an existing connection. Gatling treats the continuous stream of data from the server as a series of "events" that can be iterated over and checked. - Event Structure: Server-Sent Events have a specific format (lines starting with
data:,event:,id:,retry:). Gatling’sforeach("event".ofStream, "data")implicitly parses these based on theacceptHeader("text/event-stream"). Thedatavariable in theforeachloop will contain the content of thedata:lines. - Assertions: You can apply standard Gatling checks (like
jsonPath) to thedatapayload. If your SSE messages are not JSON, you’d use string checks (findRegex,isEqualTo, etc.). - Connection Lifetime: The
asLongAscondition is critical. Without it, Gatling would only process the first few events and then consider the scenario "finished." You need a condition that keeps the loop running as long as the stream is active and performing as expected. This could be based on response time, a specific event being received, or a timeout.
One common pitfall is forgetting the acceptHeader("text/event-stream"). Without it, the server might not send data in the SSE format, or Gatling might not interpret the response correctly as a stream. Also, the .asLongAs condition needs careful tuning; if it’s too aggressive, you might close connections prematurely, and if it’s too lenient, you might not detect stalled streams.
The next step after ensuring your stream is correctly loaded is to explore how to simulate disconnections and reconnections, a vital part of robust SSE client behavior.