Flood.io Test Scenarios: JMeter, Gatling, and Flood Element
Flood.io supports three test script types: JMeter, Gatling, and Flood Element. They're not interchangeable — each has a different execution model, resource cost, and sweet spot. This guide covers how to write scripts for each and, more importantly, when to pick which one.
JMeter on Flood
JMeter is the most portable option. If your team already has .jmx files, uploading them to Flood is the fastest path to scale. There are some behavioral differences to understand.
What Flood Does with Your JMX
When you run a JMeter test on Flood:
- Flood distributes your test across nodes in non-GUI mode
- Thread groups in your .jmx are the unit of distribution — Flood can scale thread counts per node
- GUI-only listeners (View Results Tree, Aggregate Report, Graph Results) are stripped — they don't work in distributed mode and waste resources
- Results are aggregated by Flood's collector, not JMeter's built-in aggregation
This means your .jmx should have minimal or no listeners. Results go to Flood's dashboard, not to a local file.
A Well-Structured JMeter Plan for Flood
Here's a JMeter plan designed for Flood execution — parameterized, with proper assertions, no GUI listeners:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="E-Commerce Load Test">
<boolProp name="TestPlan.functional_mode">false</boolProp>
</TestPlan>
<hashTree>
<!-- User-defined variables for parameterization -->
<Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="Variables">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.name">BASE_URL</stringProp>
<stringProp name="Argument.value">${__P(target_host,staging.example.com)}</stringProp>
</elementProp>
<elementProp name="PROTOCOL" elementType="Argument">
<stringProp name="Argument.name">PROTOCOL</stringProp>
<stringProp name="Argument.value">${__P(protocol,https)}</stringProp>
</elementProp>
</collectionProp>
</Arguments>
<hashTree/>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Browse and Purchase">
<intProp name="ThreadGroup.num_threads">50</intProp>
<intProp name="ThreadGroup.ramp_time">60</intProp>
<longProp name="ThreadGroup.duration">600</longProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Defaults">
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
<stringProp name="HTTPSampler.protocol">${PROTOCOL}</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments"/>
</elementProp>
</ConfigTestElement>
<hashTree/>
<CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="Cookie Manager">
<boolProp name="CookieManager.clearEachIteration">true</boolProp>
</CookieManager>
<hashTree/>
<!-- CSV for test data -->
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="User Credentials">
<stringProp name="filename">testdata/users.csv</stringProp>
<stringProp name="variableNames">email,password</stringProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="recycle">true</boolProp>
<boolProp name="stopThread">false</boolProp>
</CSVDataSet>
<hashTree/>
<!-- Transaction 1: Login -->
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="User Login">
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/login">
<stringProp name="HTTPSampler.path">/api/auth/login</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="email" elementType="HTTPArgument">
<stringProp name="Argument.name">email</stringProp>
<stringProp name="Argument.value">${email}</stringProp>
</elementProp>
<elementProp name="password" elementType="HTTPArgument">
<stringProp name="Argument.name">password</stringProp>
<stringProp name="Argument.value">${password}</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion testname="Login Success">
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
<intProp name="Assertion.test_type">8</intProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
</ResponseAssertion>
<!-- Extract auth token from response -->
<JSONPathExtractor testname="Extract Token">
<stringProp name="JSONPathExtractor.referenceNames">auth_token</stringProp>
<stringProp name="JSONPathExtractor.jsonPathExprs">$.token</stringProp>
</JSONPathExtractor>
</hashTree>
</hashTree>
<!-- Transaction 2: Browse products -->
<TransactionController testname="Browse Products">
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<HTTPSamplerProxy testname="GET /api/products">
<stringProp name="HTTPSampler.path">/api/products?page=1&limit=20</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion testname="Products returned">
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
<intProp name="Assertion.test_type">8</intProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
</ResponseAssertion>
</hashTree>
</hashTree>
<GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Think Time">
<stringProp name="ConstantTimer.delay">1000</stringProp>
<stringProp name="RandomTimer.range">500</stringProp>
</GaussianRandomTimer>
<hashTree/>
</hashTree>
</hashTree>
</jmeterTestPlan>Pair this with a testdata/users.csv:
email,password
user001@test.com,Pass123!
user002@test.com,Pass123!
user003@test.com,Pass123!Upload both in a zip with the structure:
my-test.zip
├── my-load-test.jmx
└── testdata/
└── users.csvJMeter Properties via Flood UI
Override target environment at launch without modifying the .jmx. In Flood's "Advanced Options," add JMeter properties:
target_host=production.example.com
protocol=httpsYour .jmx reads these with ${__P(target_host, default-value)}.
Gatling on Flood
Gatling's Akka/Netty architecture makes it significantly more efficient than JMeter for high-throughput API testing. Where a JMeter node might cap at 500-1000 virtual users, Gatling on the same hardware can often handle 5000+. The tradeoff: you write code instead of configuring a GUI.
A Realistic Gatling Simulation
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import scala.util.Random
class ECommerceSimulation extends Simulation {
val httpProtocol = http
.baseUrl("https://staging.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.userAgentHeader("Gatling Load Test")
.shareConnections
// Feeder for test users
val userFeeder = csv("testdata/users.csv").circular
val random = new Random()
val loginAndBrowse = scenario("Browse and Purchase")
.feed(userFeeder)
.exec(
http("Login")
.post("/api/auth/login")
.body(StringBody("""{"email":"${email}","password":"${password}"}"""))
.asJson
.check(status.is(200))
.check(jsonPath("$.token").saveAs("authToken"))
)
.pause(1, 3)
.exec(
http("Get product list")
.get("/api/products")
.queryParam("page", "1")
.queryParam("limit", "20")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(200))
.check(jsonPath("$.products[0].id").saveAs("productId"))
)
.pause(2, 5)
.exec(
http("Get product detail")
.get("/api/products/${productId}")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(200))
)
.pause(1, 3)
.exec(
http("Add to cart")
.post("/api/cart/items")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{"productId":"${productId}","quantity":1}"""))
.asJson
.check(status.is(201))
)
.pause(2, 4)
.exec(
http("Checkout")
.post("/api/orders")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{
"paymentMethod": "test_card",
"shippingAddress": {
"street": "123 Test St",
"city": "Testville",
"zip": "12345"
}
}"""))
.asJson
.check(status.is(201))
.check(jsonPath("$.orderId").exists)
)
// Search-heavy scenario for read path testing
val searchAndBrowse = scenario("Search and Browse")
.feed(userFeeder)
.exec(
http("Login")
.post("/api/auth/login")
.body(StringBody("""{"email":"${email}","password":"${password}"}"""))
.asJson
.check(status.is(200))
.check(jsonPath("$.token").saveAs("authToken"))
)
.repeat(10) {
exec(
http("Search products")
.get("/api/products/search")
.queryParam("q", s"product-${random.nextInt(100)}")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(200))
)
.pause(1, 2)
}
setUp(
loginAndBrowse.inject(
rampUsersPerSec(1).to(20).during(1.minute),
constantUsersPerSec(20).during(9.minutes)
),
searchAndBrowse.inject(
rampUsersPerSec(2).to(50).during(1.minute),
constantUsersPerSec(50).during(9.minutes)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(2000), // 95th percentile < 2s
global.successfulRequests.percent.gt(99) // < 1% error rate
)
}Note on setUp and Flood: When running on Flood, the platform can override your inject() parameters. If you configure user count in the Flood UI, Flood controls the injection and your setUp ramp configuration may be ignored. Check Flood's documentation for your version to understand the current behavior — it has changed across versions.
To make your simulation portable (works both locally and on Flood), read user count from a system property:
val users = System.getProperty("users", "100").toInt
val duration = System.getProperty("duration", "600").toInt.seconds
setUp(
loginAndBrowse.inject(
rampUsers(users).during(duration)
)
).protocols(httpProtocol)Uploading Gatling to Flood
Package your simulation and resources in a zip:
gatling-test.zip
├── ECommerceSimulation.scala
└── testdata/
└── users.csvFlood compiles the Scala on its nodes. This means your simulation must compile with the Gatling version Flood uses — check Flood's documentation for the current Gatling version. Dependency conflicts are the most common source of Gatling upload failures.
Flood Element (Browser-Level Load Testing)
Flood Element is fundamentally different from JMeter and Gatling. Instead of simulating HTTP traffic, it launches real Chromium browser instances and drives them with a Puppeteer-like API. This means:
- JavaScript runs, including tracking scripts, analytics, and frontend frameworks
- Resources load — CSS, images, web fonts
- Third-party requests fire
- Client-side rendering happens
The cost: browser instances are ~10-20× more resource-intensive than virtual HTTP users. A node running 500 JMeter users might run 10-20 Element users. You use Flood Element when what users actually experience matters more than raw API throughput numbers.
When Element Beats JMeter/Gatling
Use Flood Element when:
- Your app is React/Vue/Angular with client-side rendering — API load tests miss the JS execution cost entirely
- You need to test page load performance (LCP, TBT) under concurrent user load
- Third-party scripts (Segment, Intercom, GTM) affect page performance and you want to measure that
- Your test scenario involves complex interactions that are hard to replicate at the HTTP level (drag-and-drop, canvas, complex form flows)
Writing an Element Script
import {
step,
TestSettings,
Until,
By,
Browser,
Key,
} from '@flood/element'
export const settings: TestSettings = {
loopCount: -1,
description: 'E-commerce purchase flow',
screenshotOnFailure: true,
waitTimeout: 30,
actionDelay: 0.5,
stepDelay: 1,
}
export default async function(browser: Browser) {
step('Navigate to store', async () => {
await browser.visit('https://staging.example.com')
await browser.wait(Until.pageLoaded())
await browser.wait(Until.elementIsVisible(By.css('.product-grid')))
})
step('Search for product', async () => {
const searchInput = await browser.findElement(By.css('[data-testid="search-input"]'))
await searchInput.focus()
await searchInput.type('running shoes')
await browser.press(Key.Enter)
// Wait for results to load
await browser.wait(Until.elementIsVisible(By.css('[data-testid="search-results"]')))
const resultCount = await browser.findElements(By.css('[data-testid="product-card"]'))
console.assert(resultCount.length > 0, 'No search results returned')
})
step('Open product detail', async () => {
const firstProduct = await browser.findElement(
By.css('[data-testid="product-card"]:first-child [data-testid="product-link"]')
)
await firstProduct.click()
await browser.wait(Until.pageLoaded())
await browser.wait(Until.elementIsVisible(By.css('[data-testid="add-to-cart-btn"]')))
})
step('Add to cart', async () => {
const addToCartBtn = await browser.findElement(By.css('[data-testid="add-to-cart-btn"]'))
await addToCartBtn.click()
await browser.wait(Until.elementIsVisible(By.css('[data-testid="cart-notification"]')))
})
step('Proceed to checkout', async () => {
const cartIcon = await browser.findElement(By.css('[data-testid="cart-icon"]'))
await cartIcon.click()
await browser.wait(Until.pageLoaded())
const checkoutBtn = await browser.findElement(By.css('[data-testid="checkout-btn"]'))
await checkoutBtn.click()
await browser.wait(Until.pageLoaded())
await browser.wait(Until.elementIsVisible(By.css('[data-testid="payment-form"]')))
})
}Element Results
Flood Element reports response times per step() — not per HTTP request. You'll see "Navigate to store: avg 2.3s" and "Add to cart: avg 0.8s." This is closer to what users actually experience than raw API response times.
Steps that timeout (because an element never appears) are recorded as failures. screenshotOnFailure: true captures browser screenshots on step failure — invaluable for debugging intermittent failures at load.
Choosing Between JMeter, Gatling, and Element
| Factor | JMeter | Gatling | Flood Element |
|---|---|---|---|
| Learning curve | Low (GUI) | Medium (Scala) | Low (TypeScript) |
| Users per node | ~500-1000 | ~2000-5000 | ~10-20 |
| Protocol support | HTTP, JDBC, JMS, FTP, SOAP, TCP | HTTP (primarily) | HTTP (via browser) |
| Tests JS execution | No | No | Yes |
| Existing script reuse | High | Medium | Low |
| CI/CD integration | Good | Good | Good |
Pick JMeter if you have existing .jmx files, need to test non-HTTP protocols (JDBC, JMS, SOAP), or your team is comfortable with the GUI and doesn't want to write code.
Pick Gatling if you're writing new tests from scratch, testing REST/GraphQL APIs at high concurrency, and your team is comfortable with Scala or a code-first approach. Better resource efficiency means lower cost per virtual user on Flood.
Pick Flood Element if you need real browser-level measurement — page load times, JavaScript-heavy SPAs, third-party script performance, or user journey accuracy matters more than maximum concurrent users.
Mixing them on Flood: Flood supports running one script type per flood. You can't mix JMeter and Gatling in a single flood execution, but you can run sequential or overlapping floods of different types against the same application.