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.

flowchart LR subgraph GEN [Load generator] TP[bookshelf-load.jmx samplers] --> BL[Backend Listener and async queue] end TP -->|HTTP requests| API[Bookshelf API] BL -->|InfluxDB client: HTTP line protocol every 5 s| IF[(InfluxDB 1.x or 2.x)] BL -.->|Graphite client: plaintext 2003 or pickle 2004 every 1 s| GR[(Graphite)] PR[(Prometheus)] -->|scrapes /actuator/prometheus| API IF --> GF[Grafana dashboards] GR -.-> GF PR --> GF

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 BackendListenerClient class. 5.6.3 offers three core implementations: org.apache.jmeter.visualizers.backend.graphite.GraphiteBackendListenerClient, org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient and org.apache.jmeter.visualizers.backend.influxdb.InfluxDBRawBackendListenerClient. Plugins can add more. Choosing a class replaces the Parameters table with that class’s default parameters.

Graphite client

Async Queue size

Capacity of the queue of SampleResult objects waiting for the client. Saved as QUEUE_SIZE in the .jmx.

5000

Parameters

Name/value pairs passed to the client (the tables below). Values may use functions and properties, for example ${__P(influxdb_token,)}.

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)

influxdbMetricsSender

The sender class. org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender (documented) posts to the HTTP API. The 5.6.3 code also contains org.apache.jmeter.visualizers.backend.influxdb.UdpMetricsSender, which sends to an InfluxDB 1.x UDP listener; its URL is then host:port, without scheme or path.

HttpMetricsSender

influxdbUrl

Write endpoint. InfluxDB 1.x: http://host:8086/write?db=jmeter. InfluxDB 2.x and InfluxDB Cloud: http://host:8086/api/v2/write?org=<org>&bucket=jmeter. JMeter sends timestamps in nanoseconds, InfluxDB’s default precision, so no precision parameter is needed.

http://host_to_change:8086/write?db=jmeter

influxdbToken

InfluxDB 2 API token (since 5.2). When it is not blank, every request carries Authorization: Token <token>. Leave it empty for an InfluxDB 1.x server without authentication. Not in the default table: add the row yourself.

absent

application

Name of the application under test. Written as the tag application on every point, including the events annotations, so one database can hold several applications.

application name

measurement

Measurement name for the metric points.

jmeter

summaryOnly

true sends only the cumulated all row and the thread metrics; false also sends one set of rows per sampler label that matches samplersRegex. The documentation says it defaults to true; in 5.6.3 the GUI default row is false and a missing parameter is also read as false.

false

samplersRegex

Java regular expression matched with find() against each sample label, so it matches anywhere in the label unless anchored. Only used when summaryOnly is false. The cumulated all row always includes every sample.

.*

percentiles

Semicolon-separated percentiles, decimal point always . (for example 90;95;99;99.9). Three or four are enough.

99;95;90

testTitle

Text of the start and end annotations: JMeter writes <testTitle> started and <testTitle> ended to the events measurement.

Test name

eventTags

Value of the tags tag on those annotations, displayed by Grafana on each annotation. Optional.

empty

TAG_<name>

Any number of extra rows whose name starts with TAG_. Each adds the tag <name> with the row’s value to every point (metrics and annotations). Typical uses: a run identifier, the environment, the load generator host.

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 DATABASE jmeter (Influx CLI or /query HTTP API)

Create a bucket jmeter (UI, influx bucket create, or the Docker setup variables below)

influxdbUrl

http://localhost:8086/write?db=jmeter

http://localhost:8086/api/v2/write?org=bookshelf&bucket=jmeter

influxdbToken

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

Supported ( section of the server configuration)

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

application, transaction=all, statut=all, user TAG_ tags

count, countError, avg, min, max, hit, sb, rb, pct<p> for each percentile

Per matching sampler label, one row each for statut=all, ok and ko (only summaryOnly=false; a status with no samples in the interval is skipped)

application, transaction=<label>, statut, user tags

count, avg, min, max, hit, sb, rb, pct<p>

Per sampler label and distinct error, one row

application, transaction, responseCode, responseMessage, user tags

count

One thread row

application, transaction=internal, user tags

minAT, maxAT, meanAT, startedT, endedT

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 to status, but the 5.6.3 code still writes statut ("keep the old name for backward compatibility"). The raw-results client below does use status.

  • Percentile field names keep a decimal. The percentile list 90;95;99 produces the fields pct90.0, pct95.0 and pct99.0; 99.9 produces pct99.9. In InfluxQL they must be double-quoted: "pct95.0".

  • count and hit differ. count is the number of sample results in the interval (on the all row, Transaction Controller results are counted as well as the requests inside them); hit counts requests, including sub-results, and skips the Transaction Controller’s own result.

  • The all row’s avg, min and max are computed over successful samples only, while its percentiles use all samples.

  • events measurement. Annotations are points in events with the tags application, title=ApacheJMeter, the user tags and, if set, tags=<eventTags>, and a single field text.

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.

  1. Select the Test Plan and choose Add → Listener → Backend Listener. Rename it InfluxDB live metrics.

  2. In Backend Listener implementation choose org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient. The Parameters table is refilled with the defaults.

  3. Set the parameters from the table below; click Add for influxdbToken and TAG_runId.

  4. Leave Async Queue size at 5000.

  5. 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

influxdbMetricsSender

org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender

influxdbUrl

http://${__P(influxdb_host,localhost)}:8086/api/v2/write?org=${__P(influxdb_org,bookshelf)}&bucket=${__P(influxdb_bucket,jmeter)}

influxdbToken

${__P(influxdb_token,)}

application

bookshelf

measurement

jmeter

summaryOnly

false

samplersRegex

+^TX +

percentiles

90;95;99

testTitle

${__P(test_title,bookshelf-load)}

eventTags

${__P(profile,load)}

TAG_runId

${__P(run_id,local)}

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)}&amp;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

influxdbMetricsSender

org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender

HttpMetricsSender

influxdbUrl

As above; the manual’s cloud example is https://<region>.aws.cloud2.influxdata.com/api/v2/write?org=<org-id>&bucket=jmeter

http://host_to_change:8086/write?db=jmeter

influxdbToken

InfluxDB 2 token

empty

measurement

Measurement name

jmeter

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

backend_influxdb.send_interval

5

Seconds between two InfluxDB writes.

backend_graphite.send_interval

1

Seconds between two Graphite writes.

backend_influxdb.connection_timeout

1000

Connection timeout to InfluxDB, ms.

backend_influxdb.socket_timeout

3000

Socket read timeout, ms.

backend_influxdb.connection_request_timeout

100

Time to obtain a connection from the client’s pool, ms.

backend_metrics_window_mode

fixed

fixed: the last N samples, never cleared. timed: the samples of the current send interval, cleared after each send.

backend_metrics_window

100

N for fixed mode: window size for percentiles, min and max.

backend_metrics_large_window

5000

Upper bound of the window in timed mode. Too high a value can cause an out-of-memory error.

backend_metrics_percentile_estimator

LEGACY

Percentile estimation type. R_3 makes the values match the Aggregate Report.

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

graphiteMetricsSender

org.apache.jmeter.visualizers.backend.graphite.TextGraphiteMetricsSender (plaintext protocol) or org.apache.jmeter.visualizers.backend.graphite.PickleGraphiteMetricsSender (pickle protocol, more efficient for large volumes, Graphite only).

TextGraphiteMetricsSender

graphiteHost

Graphite, or InfluxDB with the Graphite plugin, host.

empty

graphitePort

Port: 2003 for plaintext, 2004 for pickle.

2003

rootMetricsPrefix

Prefix of every metric. JMeter adds no separator, so keep the trailing dot.

jmeter.

summaryOnly

Only send the cumulated metrics.

true

samplersList

Semicolon-separated sample labels, or a regular expression if useRegexpForSamplersList is true.

empty

useRegexpForSamplersList

Treat samplersList as a regular expression. In 5.6.3 the expression must match the whole label (matches()), unlike the InfluxDB client’s samplersRegex.

false

percentiles

Semicolon-separated percentiles.

90;95;99

Metric naming

Graphite metrics are dot-separated paths. With the default prefix jmeter.:

Metric Meaning

jmeter.test.minAT, jmeter.test.maxAT, jmeter.test.meanAT

Minimum, maximum and mean active threads in the interval.

jmeter.test.startedT, jmeter.test.endedT

Threads started and finished.

jmeter.<label>.ok.count, jmeter.<label>.ko.count, jmeter.<label>.a.count

Successful, failed and all responses (a = ok + ko).

jmeter.<label>.ok.min, .ok.max, .ok.avg; the same for ko and a

Response time statistics for each status.

jmeter.<label>.ok.pct<p>, .ko.pct<p>, .a.pct<p>

One metric per configured percentile, for example jmeter.all.a.pct95.

jmeter.<label>.h.count

Server hits, including sub-results. With Transaction Controllers, untick "Generate parent sample" for this to count requests.

jmeter.<label>.sb.bytes, jmeter.<label>.rb.bytes

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

graphiteMetricsSender

org.apache.jmeter.visualizers.backend.graphite.TextGraphiteMetricsSender

graphiteHost

localhost

graphitePort

2003

rootMetricsPrefix

jmeter.bookshelf.

summaryOnly

false

samplersList

TX Search only;TX Browse and borrow;TX Login heavy

useRegexpForSamplersList

false

percentiles

90;95;99

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 events annotations ("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 borrow above process_cpu_usage, hikaricp_connections_active and jvm_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).

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:

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

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/

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_generator with 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, TAG_ tags and annotations were added between 3.2 and 5.x.

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).

changes.xml on master lists no Backend Listener change at the time of writing; the clients keep their class names and parameters. Re-check before upgrading, and remember that 6.0 needs Java 17 or later.

Pitfalls

  • No data in Grafana, no error in the test. The listener never fails samples: look in jmeter.log for Error writing metrics to influxDB and the HTTP status.

  • InfluxDB 2 URL in 1.x form. /write?db=jmeter against 2.x uses the 1.x compatibility API and its own authentication rules; use /api/v2/write?org=...&bucket=... with influxdbToken, as the manual says.

  • statut, not status. Queries copied from the raw client or from the 3.3 release notes find nothing.

  • Unquoted percentile fields. SELECT pct95.0 is a syntax error in InfluxQL; write "pct95.0".

  • Regex too broad. samplersRegex uses find(), 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.

  • responseMessage cardinality. 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 timed mode or a larger window, and trust the HTML report for the final numbers.

  • Queue waits. The QueueWaits warning 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_generator tag.

  • 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