Real-time Monitoring with Backend Listener
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
The reference material consulted while preparing these pages is listed in the section bibliography.
This page was written against Apache JMeter 5.6.3 (latest release on 2026-10-14; it runs on Java 8 and later, Java 17
or later is recommended, and the examples use Java 21). JMeter 6.0 is not released: its planned changes come from
xdocs/changes.xml on the master branch, need Java 17 or later (Java 21 recommended) and are described as planned, not
as shipped behaviour. At the time of writing that file lists no change to the Backend Listener or its clients, so
everything below applies to both. The monitoring stack uses the images current on Docker Hub on the same date: InfluxDB
2.9.1 (influxdb:2.9.1, the version the latest tag points to), Grafana 13.2.4 (grafana/grafana:13.2.4) and
Prometheus from the Prometheus section of this site. InfluxDB 1.x (1.11 to 1.13) and InfluxDB 3 Core are covered where
they differ. No plugin is needed: the Backend Listener and its three clients are core JMeter.
Every parameter, property and default on this page comes from the
Backend Listener component reference,
the Real-time results chapter and the
properties reference. Where the documentation
is silent or disagrees with the code (the InfluxDB field names, the statut tag, the default of summaryOnly, how the
percentile window slides), the behaviour is read from the 5.6.3 source code in
org.apache.jmeter.visualizers.backend, and the page says so.
Why watch a test while it runs
A CLI run (the way every load test should run, as explained in
GUI & Test Plan Workflow) prints a summary + line every 30
seconds and writes a JTL file that becomes an HTML dashboard once the run ends. For a ten-minute Bookshelf load test
that is fine. For a two-hour soak test, a stress test that climbs until something breaks, or a test against a shared
environment, waiting until the end is expensive:
-
Stop early. If the error rate jumps to 30 % in minute three, there is no point in running for another hour.
-
See the shape, not the average. A summary line averages 30 seconds; a graph shows the moment p95 started to climb and whether it recovered.
-
Correlate. The interesting question is never "what was p95" but "what was the server doing when p95 doubled". That needs client-side and server-side metrics on the same time axis, in the same tool.
-
Compare runs. Metrics stored in a time-series database with a run identifier can be overlaid run against run.
JMeter’s answer since 2.13 is the Backend Listener: an asynchronous listener that aggregates sample results in the load generator and pushes the aggregates to a time-series database, from which Grafana draws live graphs. The application under test exports its own metrics (Spring Boot Actuator with Micrometer, Quarkus Micrometer) to Prometheus, and Grafana reads both.
The solid path (InfluxDB) is the one this page builds; Graphite is the older alternative and is described in its own section. The JMeter side ends at the database: the HTML dashboard described in HTML Dashboard Report is still the record of the run, and the live graphs are a view on aggregates that are deliberately lossy (see How the aggregates are computed).
The Backend Listener element
Where it sits in the plan
Add → Listener → Backend Listener. It is a listener, so it follows the listener scoping rules from Test Plan Elements, Execution Order & Scoping: placed directly under the Test Plan it receives the results of every sampler in every Thread Group; placed inside a Thread Group or controller it only receives the samples in that scope. For live monitoring it almost always belongs at Test Plan level, once. Two Backend Listeners with different names run two independent clients with two queues (for example one to InfluxDB and one to Graphite); two with the same name share one client, because the listener keeps one client and one queue per element name in the JVM.
It does not slow the samplers in the normal case: sampleOccurred only puts the result into a bounded
ArrayBlockingQueue, and a single daemon worker thread takes results out and hands them to the client in batches. The
client aggregates them in memory, and a scheduler thread sends the aggregates every few seconds. Results only reach
the samplers' threads again when the queue is full (see The async queue).
Test plan validation (Validate on a Thread Group) ignores Backend Listeners by default, so a validation run does not
write a test into the database: property testplan_validation.ignore_backends, default true.
GUI fields
| Field | Meaning | Default |
|---|---|---|
Name |
Shown in the tree. It is also the key under which the client and its queue are kept, so give each Backend Listener a distinct name. |
Backend Listener |
Comments |
Free text. |
empty |
Backend Listener implementation |
The |
Graphite client |
Async Queue size |
Capacity of the queue of |
5000 |
Parameters |
Name/value pairs passed to the client (the tables below). Values may use functions and properties, for example
|
per client |
The async queue
The queue decouples the samplers from the client. A sampler thread calls offer on the queue; if the queue is full it
counts a "queue wait" and blocks in put until the worker has made room. That blocking is the one way a Backend
Listener can distort a test: sampler threads waiting for the listener do not send requests, so throughput drops for a
reason that has nothing to do with the system under test.
The InfluxDB and Graphite clients only add each result to in-memory statistics in the worker thread, which is fast, so
the queue rarely fills with them. It fills when the worker is slow: the raw-results client (which writes every sample),
a third-party client that does I/O per batch, or a load generator whose CPU is saturated. At the end of the test, if any
wait happened, jmeter.log contains a warning of this form (from the 5.6.3 source):
WARN o.a.j.v.b.BackendListener: QueueWaits: 1834; QueueWaitTime: 912345678 (nanoseconds), you may need to increase queue capacity, see property 'backend_queue_capacity'
The message names a property backend_queue_capacity, but in 5.6.3 neither the properties reference nor
jmeter.properties defines one, and the listener reads its capacity from the Async Queue size field only. Raise
that field instead.
Worked numbers. The Bookshelf load profile (50 threads, 3 s think time, about 0.2 s response time) produces about
15.6 samples per second, so the default capacity of 5000 holds about 320 seconds of results: the worker could stall for
five minutes before a sampler waited. At 2,000 samples per second the same queue holds 2.5 seconds. Each queued
SampleResult keeps its response data until the worker has processed it, so a larger queue costs heap in proportion
to the response size; with 20 KB responses, 50,000 queued results can hold about 1 GB. Size the queue to cover a few
seconds of peak throughput, not "as large as possible", and keep response bodies small (see the listeners page for
jmeter.save.saveservice.*).
The InfluxDB client
InfluxdbBackendListenerClient (since 3.2) aggregates results per sampler label and per status and writes them to
InfluxDB in line protocol over HTTP, by default every 5 seconds. InfluxDB 2.x support (the influxdbToken parameter)
arrived in 5.2.
Parameters
| Parameter | Meaning | Default in the GUI (5.6.3) |
|---|---|---|
|
The sender class. |
|
|
Write endpoint. InfluxDB 1.x: |
|
|
InfluxDB 2 API token (since 5.2). When it is not blank, every request carries |
absent |
|
Name of the application under test. Written as the tag |
|
|
Measurement name for the metric points. |
|
|
|
|
|
Java regular expression matched with |
|
|
Semicolon-separated percentiles, decimal point always |
|
|
Text of the start and end annotations: JMeter writes |
|
|
Value of the |
empty |
|
Any number of extra rows whose name starts with |
none |
InfluxDB 1.x versus 2.x
| InfluxDB 1.x | InfluxDB 2.x | |
|---|---|---|
Container of data |
Database (plus retention policy) |
Bucket inside an organisation |
Create it |
|
Create a bucket |
|
|
|
|
not used (or 1.x user credentials in the URL, which this page does not recommend) |
an API token with write access to the bucket |
Query languages for Grafana |
InfluxQL |
Flux, or InfluxQL through a DBRP mapping (InfluxDB 2.9 creates a virtual mapping named after each bucket) |
UDP sender |
Not used: write over HTTP |
The InfluxDB 1.x database can be created with the HTTP API exactly as the JMeter manual shows:
curl -i -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE jmeter"
For InfluxDB 2.x the manual lists the three values to collect from the InfluxDB UI: the token, the bucket and the
organisation. The token belongs in a property, never in the .jmx file that goes into version control.
InfluxDB 3. InfluxDB 3 Core (influxdb:3-core) accepts the same two write endpoints for compatibility: /write
with db= and /api/v2/write with bucket=, both with Authorization: Token. The JMeter client therefore writes to
it unchanged. Grafana then reads it with SQL or InfluxQL as described in the InfluxDB 3 documentation; the community
JMeter dashboards are written for InfluxQL against 1.x and 2.x and have not been checked against 3.x here.
What the client writes
The documentation describes the metrics; the exact InfluxDB schema is in the 5.6.3 source of
InfluxdbBackendListenerClient. Every send interval it writes these points to the measurement (default jmeter):
| Rows | Tags | Fields |
|---|---|---|
One cumulated row |
|
|
Per matching sampler label, one row each for |
|
|
Per sampler label and distinct error, one row |
|
|
One thread row |
|
|
Things that surprise people when they write their own Grafana queries:
-
The status tag is spelled
statut. The 3.3 release notes announced a rename tostatus, but the 5.6.3 code still writesstatut("keep the old name for backward compatibility"). The raw-results client below does usestatus. -
Percentile field names keep a decimal. The percentile list
90;95;99produces the fieldspct90.0,pct95.0andpct99.0;99.9producespct99.9. In InfluxQL they must be double-quoted:"pct95.0". -
countandhitdiffer.countis the number of sample results in the interval (on theallrow, Transaction Controller results are counted as well as the requests inside them);hitcounts requests, including sub-results, and skips the Transaction Controller’s own result. -
The
allrow’savg,minandmaxare computed over successful samples only, while its percentiles use all samples. -
eventsmeasurement. Annotations are points ineventswith the tagsapplication,title=ApacheJMeter, the user tags and, if set,tags=<eventTags>, and a single fieldtext.
A write therefore looks like this (values illustrative, one line per point, nanosecond timestamps):
jmeter,application=bookshelf,transaction=all,statut=all,runId=load-2026-10-14 count=78,countError=0,avg=204.3,min=11.0,max=702.0,hit=74,sb=31120,rb=402884,pct90.0=351.0,pct95.0=418.0,pct99.0=640.0 1791964865000000000
jmeter,application=bookshelf,transaction=TX\ Search\ only,statut=ok,runId=load-2026-10-14 count=27,avg=188.1,min=97.0,max=544.0,hit=27,sb=8910,rb=171003,pct90.0=310.0,pct95.0=388.0,pct99.0=544.0 1791964865000000000
jmeter,application=bookshelf,transaction=internal,runId=load-2026-10-14 minAT=50,maxAT=50,meanAT=50,startedT=50,endedT=0 1791964865000000000
events,application=bookshelf,title=ApacheJMeter,runId=load-2026-10-14,tags=load text="bookshelf-load started" 1791964800000000000
Adding it to the Bookshelf plan
The Bookshelf plan names its Transaction Controllers TX Search only, TX Browse and borrow and TX Login heavy, and
its requests after their HTTP method and path (POST /api/auth/login, GET /api/books). Live graphs are most useful
per business transaction, so the listener sends per-label rows only for labels starting with TX, and the all row
for everything.
-
Select the Test Plan and choose Add → Listener → Backend Listener. Rename it
InfluxDB live metrics. -
In Backend Listener implementation choose
org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient. The Parameters table is refilled with the defaults. -
Set the parameters from the table below; click Add for
influxdbTokenandTAG_runId. -
Leave Async Queue size at 5000.
-
Save the plan. Disable the listener (Disable in the context menu) when running without the monitoring stack, or keep it enabled: a missing InfluxDB only produces errors in
jmeter.log, not failed samples.
| Parameter | Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The resulting element, as a child of the Test Plan’s hashTree:
<BackendListener guiclass="BackendListenerGui" testclass="BackendListener" testname="InfluxDB live metrics">
<elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="influxdbMetricsSender" elementType="Argument">
<stringProp name="Argument.name">influxdbMetricsSender</stringProp>
<stringProp name="Argument.value">org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="influxdbUrl" elementType="Argument">
<stringProp name="Argument.name">influxdbUrl</stringProp>
<stringProp name="Argument.value">http://${__P(influxdb_host,localhost)}:8086/api/v2/write?org=${__P(influxdb_org,bookshelf)}&bucket=${__P(influxdb_bucket,jmeter)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="influxdbToken" elementType="Argument">
<stringProp name="Argument.name">influxdbToken</stringProp>
<stringProp name="Argument.value">${__P(influxdb_token,)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="application" elementType="Argument">
<stringProp name="Argument.name">application</stringProp>
<stringProp name="Argument.value">bookshelf</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="measurement" elementType="Argument">
<stringProp name="Argument.name">measurement</stringProp>
<stringProp name="Argument.value">jmeter</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="summaryOnly" elementType="Argument">
<stringProp name="Argument.name">summaryOnly</stringProp>
<stringProp name="Argument.value">false</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="samplersRegex" elementType="Argument">
<stringProp name="Argument.name">samplersRegex</stringProp>
<stringProp name="Argument.value">^TX </stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="percentiles" elementType="Argument">
<stringProp name="Argument.name">percentiles</stringProp>
<stringProp name="Argument.value">90;95;99</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="testTitle" elementType="Argument">
<stringProp name="Argument.name">testTitle</stringProp>
<stringProp name="Argument.value">${__P(test_title,bookshelf-load)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="eventTags" elementType="Argument">
<stringProp name="Argument.name">eventTags</stringProp>
<stringProp name="Argument.value">${__P(profile,load)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="TAG_runId" elementType="Argument">
<stringProp name="Argument.name">TAG_runId</stringProp>
<stringProp name="Argument.value">${__P(run_id,local)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="classname">org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient</stringProp>
<stringProp name="QUEUE_SIZE">5000</stringProp>
</BackendListener>
<hashTree/>
The trailing space in +^TX + is intentional: it matches TX Search only but not a sampler that merely contains
"TX". Because find() is used, an unanchored TX would also match GET /api/TXT-export.
The token is passed as a property, from a properties file that is generated locally and never committed. The -q
option loads an additional properties file:
# monitoring.properties (generated by the setup script below, not committed)
influxdb_host=localhost
influxdb_org=bookshelf
influxdb_bucket=jmeter
influxdb_token=<paste the local InfluxDB token here>
jmeter -n -t bookshelf-load.jmx -q monitoring.properties \
-Jthreads=50 -Jrampup=60 -Jduration=660 -Jhost=localhost -Jport=8080 \
-Jprofile=load -Jrun_id=load-2026-10-14 -Jtest_title=bookshelf-load \
-l load.jtl
Creating summariser <summary>
Created the tree successfully using bookshelf-load.jmx
Starting standalone test @ 2026 Oct 14 10:00:00 CEST (1791964800000)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 72 in 00:00:23 = 3.1/s Avg: 201 Min: 11 Max: 702 Err: 0 (0.00%) Active: 20 Started: 20 Finished: 0
summary + 305 in 00:00:30 = 10.2/s Avg: 205 Min: 10 Max: 688 Err: 0 (0.00%) Active: 45 Started: 45 Finished: 0
summary = 377 in 00:00:53 = 7.1/s Avg: 204 Min: 10 Max: 702 Err: 0 (0.00%)
...
Tidying up ... @ 2026 Oct 14 10:11:01 CEST (1791965461000)
... end of run
The figures are illustrative. jmeter.log confirms that the listener started: it logs
InfluxDB live metrics: Starting worker with class: ... and queue capacity: 5000 and, at the end,
Sending last metrics to InfluxDB. A token passed with -J on the command line is visible in the process list and the
shell history; -q with a file readable only by the user, or an environment variable expanded by a wrapper script, is
safer.
Checking that data arrives. Before opening Grafana, query the bucket directly through the InfluxDB 2 query API (Flux):
curl -s --request POST "http://localhost:8086/api/v2/query?org=bookshelf" \
--header "Authorization: Token $INFLUXDB_TOKEN" \
--header "Accept: application/csv" \
--header "Content-Type: application/vnd.flux" \
--data 'from(bucket: "jmeter")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "jmeter" and r.transaction == "all" and r._field == "count")
|> last()'
The answer is an annotated CSV table with one row and the _value of the last count. An empty answer with HTTP 200
means nothing was written: check jmeter.log for Error writing metrics to influxDB Url: ..., responseCode: 401 (bad
token), 404 (wrong org or bucket) or failed to send data to influxDB server (unreachable host).
The raw-results client
InfluxDBRawBackendListenerClient (since 5.4) does not aggregate: it writes one point per sample result, with the
sample’s own timestamp. The manual warns that it uses more resources, both in JMeter and in InfluxDB.
| Parameter | Meaning | Default |
|---|---|---|
|
|
|
|
As above; the manual’s cloud example is |
|
|
InfluxDB 2 token |
empty |
|
Measurement name |
|
Each point has the tags status (ok or ko), transaction (the label) and threadName, and the fields
duration, ttfb (latency) and connectTime, all in milliseconds (from the 5.6.3 source). With raw points Grafana or
InfluxDB compute exact percentiles over any interval, which the aggregating client cannot. The cost: at the Bookshelf
load of about 15.6 samples per second the client writes 15.6 points per second, about 10,300 points in an 11-minute
run, which is trivial; at 2,000 samples per second it is 7.2 million points per hour, one HTTP write per worker batch,
and the series cardinality grows with every thread name. Use it for short or moderate tests, or when exact
distributions matter more than the load on the monitoring stack; use the aggregating client for long or heavy runs.
How the aggregates are computed
Both aggregating clients (InfluxDB and Graphite) keep per-label statistics and send them every send interval. The
counts (count, hit, sb, rb, errors) are reset after each send. The response-time statistics (avg, min,
max and the percentiles) are computed over a sliding window whose behaviour is set by properties:
| Property | Default | Meaning |
|---|---|---|
|
5 |
Seconds between two InfluxDB writes. |
|
1 |
Seconds between two Graphite writes. |
|
1000 |
Connection timeout to InfluxDB, ms. |
|
3000 |
Socket read timeout, ms. |
|
100 |
Time to obtain a connection from the client’s pool, ms. |
|
|
|
|
100 |
N for |
|
5000 |
Upper bound of the window in |
|
|
Percentile estimation type. |
Worked example of the default window. The Bookshelf load produces about 15.6 samples per second, or 78 per
5-second interval. In fixed mode the all percentiles describe the last 100 samples, about 6.4 seconds, which is
close to the interval. TX Login heavy is 10 % of the journeys; at about 1.5 samples per second, its 100-sample window
spans about 65 seconds, so its p95 line in Grafana is a one-minute moving percentile that reacts to a regression a
minute late and still shows it a minute after it is over. In timed mode each point describes exactly the samples of
its interval, but with 7 or 8 samples per 5 seconds for that transaction a "p99" is just the maximum. Neither mode
gives exact percentiles over the run; the HTML dashboard, computed from the JTL, does.
To make the live graphs closer to the report, either raise the window (for example backend_metrics_window=1000)
or switch to the time-boxed mode:
# user.properties on the load generator
backend_metrics_window_mode=timed
backend_metrics_large_window=5000
backend_metrics_percentile_estimator=R_3
backend_influxdb.send_interval=5
Cost on the load generator. Each tracked label keeps up to three windows (all, ok, ko) of backend_metrics_window
doubles, about 2.4 KB at the default of 100, or up to 3 × 5000 doubles (120 KB) in timed mode. Ten labels cost well
under 2 MB either way. CPU is dominated by sorting the windows for percentiles once per interval. The aggregating
clients are cheap; what costs is a high number of labels (do not let samplersRegex match dynamic labels such as
GET /api/books/1234) and the raw client at high throughput.
The Graphite client
GraphiteBackendListenerClient was the first implementation (2.13) and is still the default class in the GUI. It
sends to Graphite (Carbon) or to an InfluxDB 1.x server with the Graphite input enabled.
| Parameter | Meaning | Default |
|---|---|---|
|
|
|
|
Graphite, or InfluxDB with the Graphite plugin, host. |
empty |
|
Port: 2003 for plaintext, 2004 for pickle. |
2003 |
|
Prefix of every metric. JMeter adds no separator, so keep the trailing dot. |
|
|
Only send the cumulated metrics. |
|
|
Semicolon-separated sample labels, or a regular expression if |
empty |
|
Treat |
|
|
Semicolon-separated percentiles. |
|
Source: GraphiteBackendListenerClient parameters (Component Reference) and Graphite Sender (User Manual)
Metric naming
Graphite metrics are dot-separated paths. With the default prefix jmeter.:
| Metric | Meaning |
|---|---|
|
Minimum, maximum and mean active threads in the interval. |
|
Threads started and finished. |
|
Successful, failed and all responses ( |
|
Response time statistics for each status. |
|
One metric per configured percentile, for example |
|
Server hits, including sub-results. With Transaction Controllers, untick "Generate parent sample" for this to count requests. |
|
Sent and received bytes. |
<label> is all for the cumulated metrics of all samplers, which are always sent; labels listed in samplersList
are sent as well when summaryOnly is false. Labels and percentiles are sanitised because Graphite treats a dot as a
separator: in 5.6.3 a dot becomes _, and a space or a backslash becomes -. So 99.9 becomes pct99_9 and
TX Search only becomes jmeter.TX-Search-only.ok.count. (The manual says the dot becomes an underscore and shows
"-" in parentheses; the code uses _.) Other characters, such as / and {, are passed through: label samplers that
you send to Graphite with plain words.
A Bookshelf configuration that sends only the three transactions:
| Parameter | Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The plaintext protocol is one path value timestamp line per metric, timestamp in seconds, every second
(backend_graphite.send_interval). Values illustrative:
jmeter.bookshelf.test.meanAT 50 1791964865
jmeter.bookshelf.all.a.count 16 1791964865
jmeter.bookshelf.all.a.pct95 418.0 1791964865
jmeter.bookshelf.TX-Search-only.ok.count 5 1791964865
jmeter.bookshelf.TX-Search-only.ok.pct95 388.0 1791964865
Graphite has no tags in this scheme, so a run identifier or an application name has to go into rootMetricsPrefix
(as jmeter.bookshelf. above), and annotations are not written. Unless an existing Graphite installation is the
reason, prefer the InfluxDB client: it carries tags, annotations, error details and InfluxDB 2 authentication.
A Grafana dashboard for JMeter metrics
Installing Grafana and provisioning data sources are covered in Grafana: Dashboards & visualization; only the JMeter-specific part follows.
Community dashboard. The JMeter manual shows a Grafana dashboard without naming one. A widely used community
dashboard is Apache JMeter Dashboard using Core InfluxdbBackendListenerClient on grafana.com (ID 5496, published by
Philippe M. of Ubik Load Pack). It uses InfluxQL, asks for an InfluxDB data source pointing at the jmeter database and
for two constants, the measurement name (jmeter) and the send interval (5, which must match
backend_influxdb.send_interval). It is not part of JMeter and is not maintained by the Apache project: import it with
Dashboards → New → Import, enter 5496, choose the data source, and review the queries before relying on them.
Your own panels. Writing the few panels that matter teaches the schema and avoids surprises. These InfluxQL
queries assume the InfluxQL data source provisioned in the Compose stack below (database jmeter) and a dashboard
variable runId built from SHOW TAG VALUES FROM "jmeter" WITH KEY = "runId":
-- Throughput, requests per second (count per 5 s interval divided by 5)
SELECT sum("hit") / 5 FROM "jmeter"
WHERE "application" = 'bookshelf' AND "transaction" = 'all' AND "runId" =~ /^$runId$/ AND $timeFilter
GROUP BY time(5s) fill(0)
-- Error percentage
SELECT sum("countError") * 100 / sum("count") FROM "jmeter"
WHERE "application" = 'bookshelf' AND "transaction" = 'all' AND "runId" =~ /^$runId$/ AND $timeFilter
GROUP BY time(5s)
-- p95 per business transaction (successful samples)
SELECT mean("pct95.0") FROM "jmeter"
WHERE "application" = 'bookshelf' AND "statut" = 'ok' AND "transaction" =~ /^TX / AND "runId" =~ /^$runId$/ AND $timeFilter
GROUP BY time(5s), "transaction"
-- Active threads
SELECT last("meanAT") FROM "jmeter"
WHERE "application" = 'bookshelf' AND "transaction" = 'internal' AND "runId" =~ /^$runId$/ AND $timeFilter
GROUP BY time(5s)
-- Errors by response code and message (table panel)
SELECT sum("count") FROM "jmeter"
WHERE "application" = 'bookshelf' AND "responseCode" != '' AND $timeFilter
GROUP BY "transaction", "responseCode", "responseMessage"
-- Annotation query (Dashboard settings, Annotations, InfluxDB data source)
SELECT "text" FROM "events" WHERE "application" = 'bookshelf' AND $timeFilter
A sensible dashboard has one row per question: Is the load the one requested? (active threads, throughput), Is it
working? (error %, errors by code), How fast? (p95 and p99 per transaction, all average), and What is the server
doing? (the Prometheus panels below). Set the dashboard refresh to 5 s, the send interval; refreshing faster only
redraws the same points.
Correlating with server-side metrics
The Backend Listener measures the client side. The causes of a slowdown (CPU, heap and garbage collection, a full
connection pool, a saturated request thread pool) live in the server’s metrics. Bookshelf, as a Spring Boot
application, exposes them through Actuator and Micrometer at /actuator/prometheus; the setup, the meters and the
Prometheus scrape configuration are on
Metrics and Observability: Micrometer, Actuator, and Distributed Tracing.
A Quarkus service exposes the same kind of meters at /q/metrics, see
Observability: Health, Logging, Metrics, Tracing. Prometheus itself is covered
in Prometheus & Monitoring, starting with
Installing & running Prometheus,
Instrumenting applications and
PromQL: Functions. The workflow for reading the two timelines together
is on Performance Testing with Apache JMeter in the Spring
Boot section; it is not repeated here.
What this page adds is putting both on one Grafana dashboard:
-
One time axis. Both data sources in one dashboard share its time range. JMeter’s
eventsannotations ("started", "ended") appear as vertical markers on every panel, Prometheus panels included. -
Matching resolution. JMeter writes every 5 s; scrape Bookshelf every 5 s during tests (the Prometheus default of 1 minute hides short spikes), and use
rate(...[30s])or similar so a Prometheus point covers a few scrapes. -
Panels side by side. Put JMeter p95 for
TX Browse and borrowaboveprocess_cpu_usage,hikaricp_connections_activeandjvm_gc_pause_seconds. A latency rise that starts when the pool reaches its maximum points at the pool, not the CPU.
# PromQL panels next to the JMeter panels (Micrometer metric names)
process_cpu_usage{application="bookshelf"}
sum(hikaricp_connections_active{application="bookshelf"})
sum(rate(jvm_gc_pause_seconds_sum{application="bookshelf"}[30s]))
sum by (uri) (rate(http_server_requests_seconds_count{application="bookshelf"}[30s]))
The application label assumes that Bookshelf adds a common application tag to its meters (Spring Boot’s
management.metrics.tags.application property, or a MeterRegistry customizer); adapt the selectors to your own
labels. The server’s http_server_requests_seconds_count rate is a useful
cross-check: it should match JMeter’s throughput panel. If JMeter reports 15.6 requests per second and the server
counts 12, requests are being lost or queued in between (a proxy, a connection limit), or the load generator itself is
saturated.
PerfMon and ServerAgent (legacy)
The books monitor servers with the jmeter-plugins PerfMon Metrics Collector listener and the ServerAgent process
installed on each server (PTwJ ch. 5, Pro ch. 9). The agent uses the SIGAR native library to report CPU, memory, swap,
disk and network to the listener over TCP, and the listener draws them in the JMeter GUI. It still installs through the
Plugins Manager as jpgc-perfmon, but it has not changed since 2016 (see
Plugins & Plugins Manager), it puts an unauthenticated agent on
every server, its graphs live in the GUI listener (which should not run during a load test), and it knows nothing
about the JVM, pools or requests inside the application. The Monitor Results listener that the books use for Tomcat
was removed in 3.2. For new work use the application’s own metrics through Prometheus, plus node or container
exporters for the host (see Exporters & the Pushgateway and
Containers & Kubernetes monitoring).
Source: PerfMon (JMeter Plugins wiki)
A Docker Compose stack: JMeter, InfluxDB and Grafana next to Bookshelf
The stack below runs InfluxDB 2.9.1, Grafana 13.2.4 and Prometheus beside the Bookshelf API’s own Compose file (see
Compose for Local Development). JMeter runs on the host, as in
the rest of this section; Apache publishes no official JMeter image. Every port is bound to 127.0.0.1, so nothing is
reachable from the network, and all credentials are random local values generated by a setup script, never typed into
a file under version control.
The directory layout:
bookshelf-monitoring/
├── compose.monitoring.yaml
├── prometheus.yml
├── grafana/provisioning/datasources/datasources.yaml
├── setup-secrets.sh # generates secrets/ and .env (both in .gitignore)
├── secrets/ # influxdb2-admin-username, -password, -token, grafana-admin-password
└── .env # INFLUXDB_TOKEN=... for Grafana provisioning
1. Generate local credentials.
#!/usr/bin/env bash
# setup-secrets.sh: random, local-only credentials; run once
set -euo pipefail
mkdir -p secrets
umask 077
printf 'admin' > secrets/influxdb2-admin-username
openssl rand -hex 16 > secrets/influxdb2-admin-password
openssl rand -hex 32 | tr -d '\n' > secrets/influxdb2-admin-token
openssl rand -hex 16 > secrets/grafana-admin-password
printf 'INFLUXDB_TOKEN=%s\n' "$(cat secrets/influxdb2-admin-token)" > .env
printf 'influxdb_host=localhost\ninfluxdb_org=bookshelf\ninfluxdb_bucket=jmeter\ninfluxdb_token=%s\n' \
"$(cat secrets/influxdb2-admin-token)" > monitoring.properties
printf 'secrets/\n.env\nmonitoring.properties\n' >> .gitignore
The admin token is used for brevity on a local machine; on a shared InfluxDB create a token with write access to the
jmeter bucket only for JMeter, and a read token for Grafana.
2. The Compose file.
# compose.monitoring.yaml
include:
- ../bookshelf/compose.yaml # the Bookshelf API stack, its paths resolved from its own directory
services:
influxdb:
image: influxdb:2.9.1
ports:
- "127.0.0.1:8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME_FILE: /run/secrets/influxdb2-admin-username
DOCKER_INFLUXDB_INIT_PASSWORD_FILE: /run/secrets/influxdb2-admin-password
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN_FILE: /run/secrets/influxdb2-admin-token
DOCKER_INFLUXDB_INIT_ORG: bookshelf
DOCKER_INFLUXDB_INIT_BUCKET: jmeter
secrets:
- influxdb2-admin-username
- influxdb2-admin-password
- influxdb2-admin-token
volumes:
- influxdb2-data:/var/lib/influxdb2
- influxdb2-config:/etc/influxdb2
prometheus:
image: prom/prometheus:v3.15.0
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
grafana:
image: grafana/grafana:13.2.4
ports:
- "127.0.0.1:3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana-admin-password
INFLUXDB_TOKEN: ${INFLUXDB_TOKEN}
secrets:
- grafana-admin-password
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- grafana-data:/var/lib/grafana
depends_on:
- influxdb
- prometheus
secrets:
influxdb2-admin-username:
file: ./secrets/influxdb2-admin-username
influxdb2-admin-password:
file: ./secrets/influxdb2-admin-password
influxdb2-admin-token:
file: ./secrets/influxdb2-admin-token
grafana-admin-password:
file: ./secrets/grafana-admin-password
volumes:
influxdb2-data:
influxdb2-config:
grafana-data:
Source: Install InfluxDB with Docker Compose (InfluxDB 2), influxdb (Docker Official Image) and Configure a Grafana Docker image (Grafana documentation)
The Prometheus tag pins the release current on the date above; running and configuring the server is covered in
Installing & running Prometheus. The DOCKER_INFLUXDB_INIT_* variables
only act on the first start, with empty volumes: they create the user, the bookshelf organisation, the jmeter bucket
and the admin token. Delete the volumes (docker compose -f compose.monitoring.yaml down -v) to start over.
3. Prometheus scrapes Bookshelf. The service name and port must match the Bookshelf Compose file; here the API
service is assumed to be called bookshelf-api and to listen on 8080 inside the Compose network. The 5-second interval
matches the JMeter send interval.
# prometheus.yml
global:
scrape_interval: 5s
scrape_configs:
- job_name: bookshelf
metrics_path: /actuator/prometheus
static_configs:
- targets: ["bookshelf-api:8080"]
4. Grafana data sources. Two InfluxDB data sources (InfluxQL for the community dashboard and the queries above, Flux
for ad-hoc exploration) and Prometheus. Grafana replaces $INFLUXDB_TOKEN with the container’s environment variable
when it reads the file. InfluxQL works on InfluxDB 2.9 without extra setup, because a bucket gets a virtual DBRP
mapping with its own name, so the database is jmeter.
# grafana/provisioning/datasources/datasources.yaml
apiVersion: 1
datasources:
- name: JMeter InfluxQL
uid: jmeter-influxql
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
dbName: jmeter
httpMode: GET
httpHeaderName1: Authorization
secureJsonData:
httpHeaderValue1: Token $INFLUXDB_TOKEN
- name: JMeter Flux
uid: jmeter-flux
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: bookshelf
defaultBucket: jmeter
secureJsonData:
token: $INFLUXDB_TOKEN
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
Source: Configure the InfluxDB data source (Grafana documentation) and Provision data sources (Grafana documentation)
5. Run it. The monitoring stack has to share a network with Bookshelf for Prometheus to reach bookshelf-api. The
include element at the top of compose.monitoring.yaml (Docker Compose 2.20 or later) pulls the Bookshelf file into
the same project and network, and, unlike passing both files with two -f options (where every relative path is
resolved from the first file’s directory), keeps each file’s relative paths working:
cd bookshelf-monitoring
./setup-secrets.sh
docker compose -f compose.monitoring.yaml up -d
docker compose -f compose.monitoring.yaml ps
NAME IMAGE SERVICE STATUS PORTS
...-bookshelf-api-1 bookshelf-api:local bookshelf-api Up 20 seconds 127.0.0.1:8080->8080/tcp
...-influxdb-1 influxdb:2.9.1 influxdb Up 20 seconds 127.0.0.1:8086->8086/tcp
...-prometheus-1 prom/prometheus:v3.15.0 prometheus Up 20 seconds 127.0.0.1:9090->9090/tcp
...-grafana-1 grafana/grafana:13.2.4 grafana Up 19 seconds 127.0.0.1:3000->3000/tcp
Then run the load test from the host with the generated monitoring.properties, exactly as in
Adding it to the Bookshelf plan, and open http://localhost:3000 (user admin, password in
secrets/grafana-admin-password). Import dashboard 5496 or build the panels above, add the Prometheus panels, and set
the time range to "Last 15 minutes" with a 5 s refresh.
jmeter -n -t bookshelf-load.jmx -q bookshelf-monitoring/monitoring.properties \
-Jthreads=50 -Jrampup=60 -Jduration=660 -Jhost=localhost -Jport=8080 \
-Jprofile=load -Jrun_id=load-$(date +%Y%m%d-%H%M) -l load.jtl -e -o report/
Source: CLI Mode (User Manual)
The run feeds both outputs: the live dashboard while it runs, and the HTML report from the JTL afterwards. Keep the monitoring stack off the load generator for real tests. On one laptop, InfluxDB, Grafana, Prometheus, Bookshelf and JMeter compete for the same CPUs, which is fine to learn the tooling and wrong for measuring Bookshelf.
Running with several load generators
In a distributed test every JMeter server runs the whole plan, Backend Listener included, and each server’s client writes its own aggregates. Two consequences:
-
Tell generators apart. Add a row
TAG_generatorwith the value${__machineName()}. Without it, points from two servers with the same tags and timestamp overwrite each other in InfluxDB (a point is identified by measurement, tag set and timestamp). -
Aggregate correctly. Counts and throughput can be summed across generators. Percentiles cannot: the p95 of two p95 values is not the p95 of the combined traffic. Show per-generator percentiles, or use the raw client and compute them in InfluxDB.
Distributed testing itself is covered on a later page of this section.
From the books to 5.6 to 6.0
| Books (JMeter 2.11 to 3.0) | JMeter 5.6.3, and 6.0 as planned |
|---|---|
Server monitoring with PerfMon and ServerAgent, and the Monitor Results listener for Tomcat (PTwJ ch. 5; Pro ch. 9). |
Monitor Results was removed in 3.2. PerfMon is a legacy community plugin. Use the Backend Listener for the client side and Prometheus (Actuator, Micrometer, exporters) for the server side, in one Grafana dashboard. |
Graphite as the only built-in backend (2.13). |
InfluxDB client since 3.2, InfluxDB 2 token since 5.2, raw-results client since 5.4. Sent and received bytes, error
rows by response code and message, |
Live graphs from GUI listeners and jmeter-plugins graphs (Cookbook ch. 7). |
GUI listeners stay out of load runs. Live graphs come from Grafana; the end-of-run record is the HTML dashboard. |
JMeter 6.0 (planned). |
|
Pitfalls
-
No data in Grafana, no error in the test. The listener never fails samples: look in
jmeter.logforError writing metrics to influxDBand the HTTP status. -
InfluxDB 2 URL in 1.x form.
/write?db=jmeteragainst 2.x uses the 1.x compatibility API and its own authentication rules; use/api/v2/write?org=...&bucket=...withinfluxdbToken, as the manual says. -
statut, notstatus. Queries copied from the raw client or from the 3.3 release notes find nothing. -
Unquoted percentile fields.
SELECT pct95.0is a syntax error in InfluxQL; write"pct95.0". -
Regex too broad.
samplersRegexusesfind(), so.*(the default) sends every label, and labels that contain ids (GET /api/books/1234) create a new series per id. Name samplers with templates, not values, and anchor the regex. -
responseMessagecardinality. Error rows tag the response message; an application that puts ids or timestamps in error messages creates unbounded series. -
Percentiles that lag. The default 100-sample window turns low-volume transactions into a moving percentile over a minute or more. Use
timedmode or a larger window, and trust the HTML report for the final numbers. -
Queue waits. The
QueueWaitswarning means samplers blocked; raise Async Queue size (the property named in the message does not exist in 5.6.3) and check the generator’s CPU. -
Duplicate points from several generators. Add a
TAG_generatortag. -
Secrets in the plan. A token typed into the Parameters table is saved in clear text in the
.jmx. Use${__P(influxdb_token,)}and a local properties file. -
Monitoring stack on the generator. InfluxDB and Grafana on the load generator steal CPU from the samplers; run them elsewhere for real measurements.
References
-
Backend Listener (Apache JMeter Component Reference): GUI fields, Graphite, InfluxDB and raw InfluxDB client parameters.
-
Real-time results (Apache JMeter User Manual): metrics exposed, InfluxDB 1.x and 2 setup, Grafana, Graphite senders.
-
Properties Reference: BackendListener configuration and Properties Reference: test plan validation.
-
Getting Started (Apache JMeter User Manual): CLI options
-n,-t,-q,-J,-l,-e,-o. -
Functions (Apache JMeter User Manual):
__P,__machineName. -
Changes History (Apache JMeter): Backend Listener history from 2.13 to 5.4.
-
apache/jmeter at rel/v5.6.3: org.apache.jmeter.visualizers.backend (GitHub): queue, InfluxDB schema, Graphite sanitising and sliding windows; and
xdocs/changes.xmlon master for the planned 6.0 changes. -
InfluxDB OSS v2 documentation: Docker Compose install, HTTP API, DBRP mappings, duplicate points.
-
InfluxDB OSS v1 documentation, including UDP.
-
Configure the InfluxDB data source (Grafana), Provisioning (Grafana), Configure a Grafana Docker image (Grafana).
-
Apache JMeter Dashboard using Core InfluxdbBackendListenerClient (grafana.com, community).
-
Feeding In Your Data (Graphite documentation): plaintext and pickle protocols.
-
Bayo Erinle, JMeter Cookbook, Packt, 2014: chapter 7 (reporting with GUI listeners and plugin graphs).
-
Bayo Erinle, Performance Testing with JMeter, 2nd ed., Packt, 2015: chapter 5 (monitoring servers, PerfMon).
-
Sai Matam and Jagdeep Jain, Pro Apache JMeter, Apress, 2017: chapter 9 (PerfMon and ServerAgent).