Tools Used in SRE · Apache JMeter

Apache JMeter

Apache JMeter is the oldest tool still in active use in the load-testing category — an Apache Software Foundation project that started in 1998, years before "cloud-native" was a phrase anyone used, and it has been in continuous production use ever since. It's a pure-Java desktop application built around a GUI test-plan designer that serializes everything you build into an XML file, and its protocol support runs far past plain HTTP into databases, message queues, and file transfer — territory most of its younger, code-first competitors don't cover at all. This page walks through that GUI-and-XML model, the protocols JMeter actually speaks, the commands you run for a real test, and the operational fact every team eventually rediscovers the hard way: at high enough concurrency, JMeter's own CPU and memory become the thing you're measuring, not your service.

☺ Explain it like I'm 10

Imagine you want to find out how many kids a school doorway can let through in one minute, so you round up two hundred volunteers to walk through it over and over while one friend stands there with a stopwatch and a clipboard, writing down every single time. That works great with fifty volunteers. Push it to two hundred and your friend with the clipboard is now so busy scribbling that they can barely watch the door anymore — and if the numbers start looking slow, the honest question isn't "what's wrong with the door," it's "is my friend keeping up, or are they the bottleneck now?" Apache JMeter is that friend with the clipboard: genuinely good at counting and timing, but its own thread pool, memory, and CPU are real, finite resources too, and past a certain point they can run out of breath before the door does.

🦥Your host for this topic: Sol the Sloth — the deliberate, careful arithmetic behind capacity planning. JMeter is the tool that gets Sol the numbers, and its own slow, heavyweight nature is exactly the kind of thing Sol double-checks before trusting a single result.

What JMeter is and where it came from

☺ Like you're 10: It's been the standard way to test load since 1998 — built to test one specific Apache project, then it outgrew that and became everyone's tool.

Stefano Mazzocchi wrote the first version of JMeter in 1998 to load-test Apache JServ, the servlet engine that would later evolve into Apache Tomcat. It became an Apache Jakarta project and, eventually, a top-level Apache Software Foundation project in its own right — which makes it roughly two decades older than k6, Locust, or Gatling, all of which arrived well into the 2010s. That age shows up in two ways, one good and one worth knowing about going in. The good: an enormous body of institutional knowledge, Stack Overflow answers, enterprise support contracts, and a plugin ecosystem — the community-maintained JMeter Plugins Manager adds samplers, listeners, and timers the core project never shipped, including WebSocket, MQTT, and Kafka support. The catch: JMeter is a Java desktop application at heart, with a Swing GUI as its primary authoring surface, and that architectural choice — a real GUI, a real JVM, real OS threads per virtual user — is the throughline for almost everything distinctive about it, good and bad, covered on this page.

JMeter is not a niche tool. It's on the SRE toolchain's load/performance testing row alongside k6, Locust, and Gatling precisely because a huge number of organizations — especially ones with an existing Java estate, a QA team more comfortable with a GUI than a scripting language, or systems that need JDBC or JMS coverage no JavaScript-based tool touches — still reach for it first.

The GUI-and-XML test-plan model

☺ Like you're 10: You build the test by clicking and dragging boxes into a tree, and every click gets written down as XML behind the scenes — you're not typing code, you're assembling a diagram.

Where k6, Locust, and Gatling all start from a text editor and a scripting language, JMeter starts from a tree. Every JMeter test is a Test Plan: a hierarchy of elements you assemble in the desktop GUI by right-clicking and adding nodes, and the whole tree is saved as a .jmx file — JMeter's own XML dialect. The element types that make up that tree cover almost everything a test needs:

Here's roughly what a small slice of that tree looks like once it's saved to disk. Treat the exact element attributes as illustrative — the .jmx schema shifts in minor ways release to release, and the GUI is the authoritative way to generate one; hand-authoring this XML from scratch is technically possible and practically miserable.

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Checkout capacity test" enabled="true">
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Checkout users" enabled="true">
        <stringProp name="ThreadGroup.num_threads">200</stringProp>
        <stringProp name="ThreadGroup.ramp_time">60</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController">
          <stringProp name="LoopController.loops">10</stringProp>
        </elementProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /checkout" enabled="true">
          <stringProp name="HTTPSampler.domain">checkout.internal</stringProp>
          <stringProp name="HTTPSampler.path">/api/checkout</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
◆ Key idea

The GUI is an authoring tool, not an execution tool — and confusing the two is the single most common JMeter mistake, covered again under gotchas below. Build and debug the tree visually with a handful of threads, save the .jmx, then hand it to the non-GUI engine to actually generate load. Also worth knowing for code review: because a .jmx is generated XML rather than hand-written, diffs in version control are often noisy — a one-checkbox change in the GUI can rewrite dozens of surrounding lines — which is a real cost k6's or Locust's plain script files don't carry.

Protocol breadth well beyond HTTP

☺ Like you're 10: Most load testers only speak "web page." JMeter also speaks "database," "message queue," and "file transfer" — a much bigger vocabulary.

This is JMeter's clearest differentiator against every newer tool in the category. k6, Locust, and Gatling are all built HTTP-first (with gRPC and WebSocket support layered on more recently); JMeter shipped native, non-HTTP samplers from early in its life, because it grew up inside enterprise Java shops where "the system under test" routinely meant a database or a message broker, not a web endpoint.

SamplerProtocol / targetNotes
HTTP(S) RequestREST and web APIsThe default and most-used sampler; supports HTTP/1.1 and HTTP/2, keep-alive, and multipart bodies.
JDBC RequestAny JDBC-compliant databaseNeeds the vendor's JDBC driver .jar dropped into JMeter's lib/ directory and a JDBC Connection Configuration config element defining a pooled connection.
JMS Point-to-Point / Publisher / SubscriberMessage queues and topics — ActiveMQ, IBM MQ, and similar JMS providersRequires the provider's JMS client jars on the classpath; tests queue depth and consumer throughput under load, not just request/response latency.
FTP RequestFile transferTimes upload/download operations under concurrent load — a category none of JMeter's HTTP-first competitors cover.
TCP SamplerRaw TCP socket protocolsPluggable via a configurable TCPClient implementation for a custom wire protocol.
SOAP / XML-RPC RequestLegacy SOAP and XML-RPC servicesLargely superseded in modern tests by an HTTP Request with an XML body, but still used against older enterprise SOAP endpoints.
LDAP / LDAP ExtendedDirectory servicesBind and search operations under concurrent load.
Mail ReaderPOP3 / IMAP / SMTPTests mailbox and mail-server behavior — genuinely unusual coverage for a load-testing tool.
OS Process SamplerA local shell commandShells out and times an arbitrary command; useful for exercising a CLI tool rather than a network service.
Java Request / JSR223 (Groovy) SamplerAnything reachable from a JVM class, or arbitrary scriptingThe escape hatch — write a small Java class or a Groovy script when nothing built-in fits.

Community plugins extend this further — WebSocket, MQTT, and Kafka samplers all exist in the JMeter Plugins Manager, though they're maintained outside the core project and worth checking for update cadence before depending on one in a critical test suite.

# A JDBC Connection Configuration config element, in the values you'd actually set in the GUI:
Variable Name bound to pool:  checkoutDbPool
Database URL:                 jdbc:postgresql://db-replica.internal:5432/checkout
JDBC Driver class:             org.postgresql.Driver
Max Number of Connections:     50     # this pool is PER THREAD GROUP THREAD by default — read that twice
Max Wait (ms):                 5000
# A JDBC Request sampler then references it by the same Variable Name and runs, e.g.:
#   SELECT status FROM orders WHERE order_id = ?
# with ? parameters bound from a CSV Data Set Config for realistic, varied input.

Architecture: where the load actually comes from

☺ Like you're 10: One computer running JMeter can only pretend to be so many people at once — for a really big test, you need several computers pretending together, and something has to add up all their notes afterward.

Every JMeter thread in a Thread Group is a genuine OS-level thread inside one JVM process, each holding its own connection state, response buffers, and variable scope. That's heavier than the lightweight coroutine or event-loop models k6, Locust, and Gatling use internally — the direct cause of the resource ceiling covered under gotchas below — and it's also why JMeter has supported distributed testing since early versions: when one machine's threads aren't enough, you add machines, not more threads on the same box.

Controller jmeter -n -t plan.jmx -R worker-1,worker-2 aggregates → .jtl + HTML dashboard Worker 1 jmeter-server own JVM heap own thread pool Worker 2 jmeter-server own JVM heap own thread pool System under test checkout.internal HTTP · JDBC · JMS · FTP distribute plan requests / responses PerfMon: each worker's own CPU/heap → controller Past a few thousand threads per JVM, a worker's own CPU/heap can saturate before the target does.

Every worker in that diagram needs an identical .jmx and matching JMeter version and JDK — a mismatch is a common source of a distributed run that silently behaves differently on one node than the others. The controller's job is purely to fan the test out and aggregate results back; it does not generate load itself in a distributed run, and treating it as just another worker under-counts real capacity.

The commands you actually run

☺ Like you're 10: Build and check your test with the picture-clicking window, but the real test always runs with the window closed.

The GUI is for building and debugging a test plan with a handful of threads — it is never the way you generate real load, for reasons covered under gotchas next. The engine you actually run load through is non-GUI mode.

# the standard way to run a real test: non-GUI, write raw samples to a .jtl,
# then generate the HTML Dashboard Report from that .jtl when it's done
$ jmeter -n -t checkout-capacity.jmx -l results.jtl -e -o report/

# give the JVM real headroom before you scale threads up — the default heap is small
$ JVM_ARGS="-Xms1g -Xmx4g" jmeter -n -t checkout-capacity.jmx -l results.jtl

# override a test-plan property from the command line without editing the .jmx
$ jmeter -n -t checkout-capacity.jmx -Jusers=500 -Jrampup=120 -l results.jtl

# DISTRIBUTED: start the engine on each worker first (defaults to RMI port 1099)
worker$ jmeter-server

# then drive them all from the controller — same .jmx, same JMeter version on every node
controller$ jmeter -n -t checkout-capacity.jmx -R worker-1,worker-2 -l results.jtl -e -o report/

# regenerate just the HTML dashboard from an existing .jtl, without re-running anything
$ jmeter -g results.jtl -o report/

# sanity-check a test plan's structure before you run it at all
$ jmeter -n -t checkout-capacity.jmx --report-output-folder /tmp -l /dev/null -e -o /tmp/report -n

Watching the generator's own health during a run is not optional at real scale. The community PerfMon Server Agent plugin runs a lightweight agent on each load-generating machine and feeds its CPU, memory, and network numbers into a PerfMon listener back on the controller — the direct way to answer "is it them or is it us" without guessing, the exact question the schematic above and the gotcha below both turn on.

Gotchas and failure modes

☺ Like you're 10: Most JMeter surprises come from treating it like a lightweight tool when it's actually a fairly heavy one — the fixes are almost all "run it a leaner way," not "the tool is broken."

⚠ The load generator is a variable in the experiment, not a wall

This is the headline operational reality of running JMeter at scale, and it's the reason this page exists in a capacity-planning context rather than only a tooling reference. Each JMeter thread is a real OS thread with its own stack, connection state, and response buffer inside one JVM — nothing like the lightweight goroutines, greenlets, or actors k6, Locust, and Gatling use. As concurrency climbs, JMeter's own CPU and heap climb with it, and at some point — commonly in the low thousands of threads per JVM node, though the exact number depends heavily on heap size, response payload size, and how many listeners and assertions are attached — the generator saturates before the system under test does. A latency graph that looks like your service degraded can just as easily be JMeter itself running out of breath. The fix isn't a JMeter setting; it's discipline: watch the generator's own CPU and heap with PerfMon on every real run, and treat a saturating generator as a scaling problem for the generator (add distributed workers, or switch to a lighter-weight tool) rather than evidence about the target.

Alternatives and when to reach for something else

☺ Like you're 10: The newer tools trade away some of JMeter's protocol breadth and GUI-friendliness for being lighter on their feet — which one wins depends on what you're testing and who's writing the test.

ToolConcurrency modelAuthoringProtocol breadthBest when
JMeterReal OS threads, one per virtual user — heaviest per-VU footprint of the fourGUI test-plan tree, saved as XML; JSR223/Groovy as an escape hatchWidest by far: HTTP, JDBC, JMS, FTP, TCP, LDAP, mail, plugins for moreNon-HTTP protocols matter, a QA team prefers a GUI over code, or decades of institutional .jmx files already exist
k6Goroutines — lightweight, high VU count per nodeJavaScript, plain code files, CI-nativeHTTP/1.1, HTTP/2, gRPC, WebSocketDeveloper-authored tests living in the same repo as the service, tight CI integration
Locustgevent greenlets — very lightweight, distributed via a built-in controller/worker modePython, plain codeHTTP-first; anything reachable via a custom Python client classA Python-fluent team wants full programming-language control over user behavior
GatlingAkka actors — very high throughput per nodeScala DSLHTTP, WebSocket, JMS, gRPCMaximum load per generator box matters and a Scala-comfortable team is writing the tests

The practical rule most teams land on: reach for JMeter when the system under test isn't purely an HTTP API — a database connection pool, a message broker, an FTP endpoint — or when non-programmers need to build and maintain tests through a GUI; reach for k6, Locust, or Gatling when the target is HTTP/gRPC-shaped, the team writing tests is comfortable in code, and squeezing the most virtual users out of the fewest generator nodes matters more than protocol breadth. Plenty of mature testing practices keep both: JMeter for the JDBC and JMS suites nothing else covers, a lighter tool for everyday HTTP regression load tests in CI. See the SRE toolchain for how the whole load-testing category sits next to monitoring and paging tools, and queueing theory for SRE for the arrival-rate math a good load test should actually be validating.

🎬 At the Reliability Watch
🦥

Sol the Sloth: Two thousand threads, ramped over sixty seconds, and checkout's p99 just cracked four hundred milliseconds. That's over budget.

🦊

Foxy: Over budget for checkout, or over budget for whatever's running the test? Did anyone actually watch the load generator's own CPU while this ran?

🦥

Sol the Sloth: ...I did not. Let me check the PerfMon numbers before I write this down as a finding.

🦫

Benny the Beaver: While Sol checks, I'll say it — hand-editing that .jmx XML for the fortieth time is exactly the kind of toil I'd script away if I could. I can't; the GUI is how you're supposed to build it.

🦥

Sol the Sloth: ...Confirmed. The generator box was pinned at 98% CPU for the last twenty seconds of that run. Checkout was fine. We were the bottleneck.

🐢

Timmy the Turtle: So the real next step isn't a fix in checkout at all — it's adding a second worker node before we trust this number either way.

🦥

Sol the Sloth: Agreed. Slowly, correctly, one variable at a time — that's the only way this arithmetic means anything.

Going further

☺ Like you're 10: This page covers the tool; the next pages cover what the numbers it gives you are actually for.

The canonical source is the Apache JMeter user manual at jmeter.apache.org — confirm exact element names, current sampler lists, and version-specific behavior there, since JMeter's own release notes are the authority on anything this page simplified for space. Pair this page with capacity planning & performance for what a load test's results are actually for, queueing theory for SRE for the arrival-rate math behind a realistic test design, and the SRE toolchain for how JMeter sits next to k6, Locust, and Gatling in the broader tool landscape. For hands-on practice, work through Drill — Forecast the Bottleneck and Capstone Part 5 — Capacity Plan & Load Test.

✓ Checkpoint

1. What does JMeter save a test plan as, and what's the practical downside of that format in version control? 2. Name three protocols JMeter can load-test that k6, Locust, and Gatling generally can't reach natively. 3. Why should real load never be generated from the JMeter GUI? 4. What's the core resource reason JMeter's concurrency model is heavier than k6's or Locust's, and what tool would you use to confirm the load generator — not the target — has saturated? 5. What's the difference between a fixed-concurrency Thread Group and a fixed-arrival-rate test, and why does that distinction matter for a realistic capacity test?

Check your answers
  1. A .jmx XML file generated by the GUI test-plan tree. Because it's generated rather than hand-written, a small GUI change can rewrite large chunks of the file, producing noisy, hard-to-review diffs compared to a plain script file.
  2. Any three of: JDBC (databases), JMS (message queues/topics), FTP (file transfer), LDAP (directory services), or mail protocols (POP3/IMAP/SMTP) — all native JMeter samplers with no equivalent built into the HTTP-first tools.
  3. The Swing GUI itself consumes CPU and memory rendering results as they stream in, which competes with and distorts the very threads it's supposed to be driving accurately once concurrency climbs past a small number.
  4. Each JMeter thread is a real OS-level thread with its own stack and buffers inside one JVM, far heavier than the goroutines or greenlets k6 and Locust use — so JMeter's own CPU and heap climb faster with concurrency. The PerfMon Server Agent, watching the generator's own CPU/memory during a run, is what confirms whether the generator or the target actually saturated first.
  5. A standard Thread Group holds the number of concurrent virtual users fixed and lets throughput vary with response time (a closed model); a fixed-arrival-rate test holds the rate new requests arrive at fixed instead, closer to how most real production traffic actually behaves. Testing only the closed-model default can understate how a system behaves under sustained real-world arrival pressure, particularly near saturation.