Messaging: JMS, Mail & TCP
|
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), with the differences planned for JMeter 6.0 noted where they matter. JMeter 6.0 is not
released: its planned changes come from xdocs/changes.xml and the source on the master branch, it needs Java 17 or
later (Java 21 recommended), and everything said about it here is planned behaviour, not shipped behaviour. The JMS
examples use a local Apache ActiveMQ Classic broker (Docker image apache/activemq-classic:5.19.2) with the ActiveMQ
Classic 5.19.x client jar inside JMeter; the mail examples use a local GreenMail 2.1.14 test mail server; the TCP
examples use a local socat echo server. Every field name, default and rule is taken from the official component
reference and the two JMS tutorials; where the documentation is silent (JMX property names, the stored values of radio
buttons), the 5.6.3 source code is the reference, and that is said explicitly. All credentials are placeholders and
every target is localhost.
Most of the Bookshelf section load-tests the HTTP API. The Bookshelf platform has other moving parts, though: loan requests are handed to a loan worker through a message queue, loan events are broadcast on a topic to the notification and analytics services, confirmation e-mails go through a mail server, and stock levels still come from a legacy line-based TCP service. JMeter has core samplers for all of these. This page covers the three JMS samplers, the two mail samplers and the TCP Sampler: what each one measures, every field, how to wire them to a local broker or server, what they cost on the load generator, and the traps that make a messaging load test report fiction.
The concepts behind the brokers themselves (queues versus topics, delivery guarantees, what JMS is and is not) are covered in the Messaging Reference; in particular Messaging Models and Patterns explains point-to-point and publish/subscribe, Messaging Protocols explains why JMS is an API and not a wire protocol, Delivery Guarantees covers persistence and acknowledgement, and Spring Boot: Other Messaging Options shows the application side of JMS with ActiveMQ Artemis. This page only adds what JMeter brings.
What these samplers measure
A JMeter sampler measures one interaction from the client’s point of view. For HTTP that is obvious: request out, response back. For messaging it depends on the style of interaction, and the choice decides what the numbers mean.
| Sampler and mode | One sample is… | What its elapsed time means |
|---|---|---|
JMS Point-to-Point, Request Only |
Sending one message to a queue |
How long the broker takes to accept the message (for persistent delivery, including the broker’s store write). Says nothing about when, or whether, it is consumed. |
JMS Point-to-Point, Request Response |
Sending a request and waiting for the matching reply |
End-to-end round trip through the broker and the service that consumes the request and replies. The only JMS mode that measures a service’s response time. |
JMS Publisher |
Publishing Number of samples to aggregate messages to a topic or queue |
Time to publish that batch. |
JMS Subscriber |
Receiving Number of samples to aggregate messages from a topic or queue |
Mostly the time spent waiting for messages to arrive. It is a throughput and delivery check, not a latency measurement of any server. |
SMTP Sampler |
One complete SMTP session that sends one message |
Connect, optional TLS and authentication, and the transfer of one message to the mail server. |
Mail Reader Sampler |
One complete POP3 or IMAP session that reads some or all messages of a folder |
Connect, log in, open the folder, fetch the messages (and optionally delete them). |
TCP Sampler |
Sending a block of bytes on a socket and reading one response |
Time until the response is considered complete, which depends on how the client implementation detects the end of a response. |
The rest of the page goes through the samplers in that order. All of them are added with Add → Sampler → … on a Thread Group or a controller, and follow the usual scoping and execution order described on Test Plan Elements, Execution Order & Scoping: config elements, pre-processors and timers in scope run before the sampler, post-processors and assertions after it.
Source: Samplers (Component Reference)
A local test bed
Load tests are only ever aimed at the reader’s own systems. The test bed below runs a JMS broker and a mail server on
the developer machine, bound to 127.0.0.1 so nothing is exposed on the network. Both images are official or
project-published images: apache/activemq-classic is published by the ActiveMQ project, and greenmail/standalone
by the GreenMail project.
# compose.yaml -- local brokers for the JMeter messaging examples
services:
activemq:
image: apache/activemq-classic:5.19.2
environment:
# Enables authentication on the broker's transport connectors.
# Placeholder values: replace them, and never commit real ones.
ACTIVEMQ_CONNECTION_USER: loadtest
ACTIVEMQ_CONNECTION_PASSWORD: change-me
ports:
- "127.0.0.1:61616:61616" # OpenWire (JMS clients)
- "127.0.0.1:8161:8161" # web console, to watch queue depths
greenmail:
image: greenmail/standalone:2.1.14
environment:
GREENMAIL_OPTS: >-
-Dgreenmail.setup.test.all
-Dgreenmail.hostname=0.0.0.0
-Dgreenmail.users=reader1:change-me@bookshelf.test
-Dgreenmail.users.login=email
ports:
- "127.0.0.1:3025:3025" # SMTP
- "127.0.0.1:3110:3110" # POP3
- "127.0.0.1:3143:3143" # IMAP
docker compose up -d
docker compose ps
NAME IMAGE SERVICE STATUS PORTS
messaging-activemq-1 apache/activemq-classic:5.19.2 activemq Up 5 seconds 127.0.0.1:8161->8161/tcp, 127.0.0.1:61616->61616/tcp
messaging-greenmail-1 greenmail/standalone:2.1.14 greenmail Up 5 seconds 127.0.0.1:3025->3025/tcp, 127.0.0.1:3110->3110/tcp, 127.0.0.1:3143->3143/tcp
ACTIVEMQ_CONNECTION_USER and ACTIVEMQ_CONNECTION_PASSWORD switch on authentication for JMS clients (without them,
the image accepts anonymous connections); GREENMAIL_OPTS configures one mailbox, reader1@bookshelf.test, whose login
is the full e-mail address because of greenmail.users.login=email. The TCP examples need a third, even smaller server,
started later on this page.
The credentials reach JMeter as run properties, never as literals in the .jmx file. The JMS and mail samplers all
store their password fields unencrypted in the test plan (the component reference says so for each of them), so a
literal password in a committed plan is a leaked password:
# messaging.properties -- passed with -q messaging.properties (placeholder values)
jms_url=tcp://localhost:61616
jms_user=loadtest
jms_password=change-me
mail_user=reader1@bookshelf.test
mail_password=change-me
tcp_host=localhost
tcp_port=7000
JMS in JMeter
JMS (Java Message Service, now Jakarta Messaging) is a Java API, not a protocol: every broker ships its own client library that implements the API and speaks the broker’s wire protocol. JMeter’s three JMS samplers are written against the API only, so they work with any broker whose JMS client jar is on JMeter’s classpath, and they find the broker’s objects (connection factory, queues, topics) through JNDI.
| Sampler | Use it for |
|---|---|
JMS Point-to-Point |
Queues. Sends messages and, optionally, waits for replies: request only (fire and forget), request/response with correlation, plus Read, Browse and Clear utilities. |
JMS Publisher |
Sending messages to a topic (publish/subscribe) or a queue, from inline text, a file or a random file from a folder; text, map, object or bytes messages. |
JMS Subscriber |
Receiving messages from a topic or a queue, with an optional durable subscription and message selector. |
The official tutorials build one plan of each kind: the point-to-point tutorial uses one Thread Group of 5 threads, each sending 4 messages through a request queue and waiting on a fixed reply queue; the topic tutorial uses a Subscribers Thread Group of 5 threads and a Publisher Thread Group of 1 thread, 10 iterations each. The Bookshelf examples below follow the same shapes with the Bookshelf loan flow.
Provider jars in lib
JMeter includes the JMS API jar, but no JMS client implementation. The provider’s jars go in JMeter’s lib directory,
not in lib/ext (which is for plugins), or in a directory listed in the user.classpath property; JMeter must be
restarted after adding them. Setting the CLASSPATH environment variable has no effect, because JMeter is started with
java -jar. For ActiveMQ the getting-started guide asks for the single activemq-all-X.X.X.jar.
The version of the client jar matters more than the guide suggests, because the JMS API changed package name:
| JMeter | JMS API it is compiled against | Compatible client jars |
|---|---|---|
5.6.3 (current) |
|
Clients that implement |
6.0 (planned) |
|
Clients that implement |
The 5.6.3 column is from the 5.6.3 build files (geronimo-jms_1.1_spec); ActiveMQ Classic 6.x moved its client to the
Jakarta Messaging API, so a 6.x client jar dropped into JMeter 5.6.3 does not provide the javax.jms classes the
samplers call. The broker version is a separate question: a 5.19 client talks OpenWire to a 5.19 or 6.x broker
alike. The 6.0 column is from the master branch, whose JMS module now depends on jakarta.jms-api 3.1.0 and imports
jakarta.jms.*; at the time of writing this is not yet listed in changes.xml, so treat it as planned and re-check
the 6.0 release notes before upgrading. A plan written for 5.6.3 keeps working on 6.0 as far as the .jmx file is
concerned; it is the jars in lib that will need replacing.
# Install the ActiveMQ Classic 5.19.x all-in-one client into JMeter 5.6.3 (download it from the ActiveMQ site
# or Maven Central and verify its checksum first)
cp activemq-all-5.19.2.jar "$JMETER_HOME/lib/"
ls "$JMETER_HOME/lib" | grep -i -E 'jms|activemq'
activemq-all-5.19.2.jar
geronimo-jms_1.1_spec-1.1.1.jar
JNDI: how the samplers find the broker
All three samplers look up a connection factory and destinations through a JNDI InitialContext. They can build it in
two ways:
-
From fields in the sampler: an Initial Context Factory class name, a Provider URL, and (Point-to-Point only) a table of extra JNDI Properties.
-
From a
jndi.propertiesfile (Publisher and Subscriber only, through the Use jndi.properties file checkbox): the file must be on the classpath, for example in a directory added touser.classpath; the context-factory and provider-URL fields are then disabled.
For ActiveMQ Classic, the component reference gives these values:
| Item | Value | Comment |
|---|---|---|
Context factory |
|
|
Provider URL |
|
|
Provider URL |
|
Embedded broker with persistence disabled |
Queue reference |
|
Defines the queue in JNDI dynamically, without configuration |
Topic reference |
|
Defines the topic in JNDI dynamically, without configuration |
The default connection factory name in ActiveMQ’s JNDI context is ConnectionFactory. Fixed JNDI names are defined
with queue.<jndiName>=<physicalName> and topic.<jndiName>=<physicalName> entries, which is what the point-to-point
tutorial does (queue.Q.REQ=example.A, queue.Q.RPL=example.B). An embedded vm:// broker is handy for a quick
experiment, but it runs inside the load generator and competes with it for CPU and heap; a load test should always use
a separate broker.
The Bookshelf jndi.properties, for the Publisher and Subscriber when the checkbox is used:
# jndi.properties -- in a directory listed in user.classpath
java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
java.naming.provider.url=tcp://localhost:61616
queue.LoanRequests=bookshelf.loan.requests
queue.LoanReplies=bookshelf.loan.replies
topic.LoanEvents=bookshelf.loan.events
# user.properties
user.classpath=../lib;/opt/bookshelf-loadtest/jndi
For ActiveMQ Artemis the equivalent JNDI factory is org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory,
and connection factories are declared as connectionFactory.<name>=tcp://host:port entries, with the same queue.
and topic. prefixes for destinations; with JMeter 5.6.3 use Artemis’s javax.jms client, not its Jakarta client.
JMS Point-to-Point
The JMS Point-to-Point sampler sends, and optionally receives, messages through queues. The component reference sums up its modes: request only typically puts load on a JMS system, while request/reply is used to test the response time of a JMS service that processes messages from the request queue, because it waits for the response on the reply queue.
Communication styles
| Style | JMX value (JMSSampler.communicationStyle) |
Behaviour |
|---|---|---|
Request Only |
|
Sends messages and does not monitor replies ("fire and forget"). Use it to load a broker or a consumer service. |
Request Response |
|
Sends a message and waits for the matching reply. If JNDI name Receive queue has a value, that fixed queue is
monitored and the reply is matched to the request through message and correlation IDs. If it is empty, JMeter uses a
temporary queue, and the sending thread blocks until the reply arrives. Either way a server must listen on the request
queue and send replies to the queue in the message’s |
Read |
|
Reads a message from a queue that has no listener attached (useful in tests). Combined with Number of samples to aggregate, it reads several. |
Browse |
|
Returns the current queue depth without removing messages. |
Clear |
|
Removes all messages from the queue. |
The numeric values are the 5.6.3 source’s JMS_COMMUNICATION_STYLE enum; older plans contain a
JMSSampler.isFireAndForget boolean from before Read, Browse and Clear existed (they arrived in JMeter 4.0).
Correlation: matching a reply to its request
With a fixed reply queue, many threads wait on the same queue, so JMeter must know which reply belongs to which request. Two checkboxes under Use alternate fields for message correlation decide which header fields are compared:
-
Use Request Message Id: if selected, the request’s
JMSMessageIDis used, otherwise the request’sJMSCorrelationID(which then has to be set on the request, through the JMS Properties table). -
Use Response Message Id: if selected, the response’s
JMSMessageIDis used, otherwise the response’sJMSCorrelationID.
| Pattern | Use Request Message Id | Use Response Message Id | The service must… |
|---|---|---|---|
JMS Message ID pattern (most common) |
selected |
cleared |
copy the request’s |
JMS Correlation ID pattern |
cleared |
cleared |
copy the request’s |
Same queue for request and reply (raw broker throughput) |
selected |
selected |
nothing: the "reply" is the request message itself, so both message IDs match. |
In both real patterns the application is responsible for populating the correlation ID. A correlation ID can be set on
the request by adding a row named JMSCorrelationID to the JMS Properties table (the name is matched case
insensitively in the 5.6.3 source, which also treats a JMSType row as the JMSType header since JMeter 5.4).
With the Request Response style and a reply queue, the sampler sets the request’s JMSReplyTo to that queue; one
receiver per reply queue hands each reply to the thread waiting for its ID. Replies that arrive after the Timeout are
discarded and the sample fails with the response message No reply message received.
Every field
The JMX property names are from the 5.6.3 source (JMSSampler.java); the field labels and descriptions are from the
component reference.
| Field | JMX property | Meaning |
|---|---|---|
Name |
|
Label in the tree and in results. |
QueueConnection Factory |
|
JNDI name of the queue connection factory. Required. |
JNDI name Request queue |
|
JNDI name of the queue the messages are sent to. Required. |
JNDI name Receive queue (component reference: "JNDI Name Reply queue") |
|
JNDI name of the reply queue. Used by Request Response; empty means temporary queues. |
Number of samples to aggregate |
|
Only for the Read style: how many messages one sample reads. Default |
JMS Selector |
|
Message selector (the SQL-92 subset defined by JMS) to take only matching messages. |
Communication style |
|
Request Only, Request Response, Read, Browse or Clear (see above). |
Use Request Message Id, Use Response Message Id |
|
Correlation fields (see above). |
Timeout (ms) |
|
Reply timeout. Default 2000 ms; |
Expiration (ms) |
|
Message time to live. Default |
Priority (0-9) |
|
Message priority, from 0 (lowest) to 9 (highest). Default 4. |
Use non-persistent delivery mode? |
|
Sends with |
Content |
|
The message body (a text message). JMeter functions and variables are evaluated. |
JMS Properties |
|
Message properties: name, value and Class of value ( |
Initial Context Factory |
|
Class of the JNDI context factory. |
JNDI Properties |
|
Extra properties for the JNDI implementation, such as |
Provider URL |
|
URL of the JMS provider. |
There are no user and password fields. JMeter uses the JNDI properties java.naming.security.principal and
java.naming.security.credentials, if present, when it creates the queue connection; setting the JMeter property
JMSSampler.useSecurity.properties=false disables that.
Example 1: raw broker throughput on one queue
The smallest runnable JMS test needs nothing but the broker: request and reply on the same queue, with both correlation checkboxes selected, so every request comes back to its own thread as its "reply". It measures the broker’s send-plus-receive cost, which is a useful baseline before a service is involved.
-
Copy the client jar into
libas shown above and start JMeter. -
Add → Threads (Users) → Thread Group: name
Broker baseline, Number of Threads${__P(threads,1)}, Ramp-up period${__P(rampup,1)}, Loop Count Infinite, Specify Thread lifetime with Duration${__P(duration,60)}. -
Add → Sampler → JMS Point-to-Point on it, and fill in:
-
QueueConnection Factory:
ConnectionFactory -
JNDI name Request queue and JNDI name Receive queue:
dynamicQueues/bookshelf.baseline -
Communication style: Request Response; both Use Request Message Id and Use Response Message Id selected
-
Timeout (ms):
5000; Content:ping -
Initial Context Factory:
org.apache.activemq.jndi.ActiveMQInitialContextFactory -
Provider URL:
${__P(jms_url,tcp://localhost:61616)} -
JNDI Properties:
java.naming.security.principal=${__P(jms_user,)}andjava.naming.security.credentials=${__P(jms_password,)}
-
-
Add → Assertions → Response Assertion below the sampler, Text Response Contains
ping.
<JMSSampler guiclass="JMSSamplerGui" testclass="JMSSampler" testname="P2P baseline" enabled="true">
<stringProp name="JMSSampler.queueconnectionfactory">ConnectionFactory</stringProp>
<stringProp name="JMSSampler.SendQueue">dynamicQueues/bookshelf.baseline</stringProp>
<stringProp name="JMSSampler.ReceiveQueue">dynamicQueues/bookshelf.baseline</stringProp>
<intProp name="JMSSampler.communicationStyle">1</intProp>
<boolProp name="JMSSampler.useReqMsgIdAsCorrelId">true</boolProp>
<boolProp name="JMSSampler.useResMsgIdAsCorrelId">true</boolProp>
<boolProp name="JMSSampler.isNonPersistent">false</boolProp>
<stringProp name="JMSSampler.timeout">5000</stringProp>
<stringProp name="HTTPSamper.xml_data">ping</stringProp>
<stringProp name="JMSSampler.initialContextFactory">org.apache.activemq.jndi.ActiveMQInitialContextFactory</stringProp>
<stringProp name="JMSSampler.contextProviderUrl">${__P(jms_url,tcp://localhost:61616)}</stringProp>
<elementProp name="JMSSampler.jndiProperties" elementType="Arguments" guiclass="ArgumentsPanel"
testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="java.naming.security.principal" elementType="Argument">
<stringProp name="Argument.name">java.naming.security.principal</stringProp>
<stringProp name="Argument.value">${__P(jms_user,)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="java.naming.security.credentials" elementType="Argument">
<stringProp name="Argument.name">java.naming.security.credentials</stringProp>
<stringProp name="Argument.value">${__P(jms_password,)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<elementProp name="arguments" elementType="JMSProperties">
<collectionProp name="JMSProperties.properties"/>
</elementProp>
</JMSSampler>
jmeter -n -t bookshelf-jms-baseline.jmx -q messaging.properties -Jthreads=10 -Jduration=60 -l jms-baseline.jtl
Creating summariser <summary>
Created the tree successfully using bookshelf-jms-baseline.jmx
Starting standalone test @ 2026 Oct 14 11:00:00 CEST (1791968400000)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 24518 in 00:00:29 = 845.4/s Avg: 11 Min: 2 Max: 148 Err: 0 (0.00%) Active: 10 Started: 10 Finished: 0
summary + 25890 in 00:00:30 = 863.0/s Avg: 11 Min: 2 Max: 97 Err: 0 (0.00%) Active: 0 Started: 10 Finished: 10
summary = 50408 in 00:00:59 = 854.4/s Avg: 11 Min: 2 Max: 148 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 11:01:00 CEST (1791968460000)
... end of run
The numbers are illustrative and depend on the machine. With no think time, each of the 10 threads does about 1000 / 11 ms ≈ 90 round trips per second, which matches the ≈ 850/s total: in a closed model with no pauses, throughput is just threads divided by response time. Because messages are persistent by default, most of those 11 ms are the broker’s store writes; selecting Use non-persistent delivery mode? usually makes the same run noticeably faster, which is exactly why the delivery mode of the test must match production.
Example 2: Bookshelf loan requests with request/reply
The real Bookshelf loan worker listens on bookshelf.loan.requests, creates the loan, and replies to the request’s
JMSReplyTo queue with the request’s JMSMessageID as JMSCorrelationID: the JMS Message ID pattern. The test
measures the round trip a borrower’s request takes through the broker and the worker.
The plan follows the point-to-point tutorial (JNDI names mapped through JNDI Properties), with the Bookshelf values:
| Field | Value | Why |
|---|---|---|
QueueConnection Factory |
|
ActiveMQ’s default JNDI name |
JNDI name Request queue |
|
Mapped by |
JNDI name Receive queue |
|
Fixed reply queue, mapped by |
Communication style |
Request Response |
A worker must reply |
Use Request Message Id / Use Response Message Id |
selected / cleared |
JMS Message ID pattern |
Timeout (ms) |
|
Above the loan SLA, so slow replies are measured rather than dropped |
Content |
|
One loan request per sample |
JNDI Properties |
|
JNDI names to physical queues, and credentials |
Provider URL |
|
Local broker |
Add a Constant Timer of 1000 ms under the sampler (think time) and a Response Assertion on "status":"ACTIVE".
Worked numbers. With 20 threads, a 1 s think time and a typical 50 ms round trip, each thread completes one loan every 1.05 s, so the plan offers 20 / 1.05 ≈ 19 loan requests per second; the worker must process a request in under 1000 / 19 ≈ 52 ms on average just to keep the request queue from growing. Watch the queue depth in the ActiveMQ web console (or with a Browse sample) during the run: a depth that keeps rising means the worker, not JMeter, is the bottleneck, and the round-trip times will climb with it.
A stub worker for trying the plan without the real service. When the loan worker is not available, a small Groovy
responder can stand in for it. It is not an official JMeter pattern, only a test stub; run it as a separate plan in a
separate JMeter process (one Thread Group of 1 to 2 threads, Loop Count Infinite, one JSR223 Sampler with language
groovy), so its samples do not pollute the load test’s results:
import javax.jms.Session
import org.apache.activemq.ActiveMQConnectionFactory
// Created once per responder thread, on its first iteration, and reused afterwards
def session = vars.getObject('stubSession')
if (session == null) {
def factory = new ActiveMQConnectionFactory(props.getProperty('jms_url', 'tcp://localhost:61616'))
def connection = factory.createConnection(props.getProperty('jms_user'), props.getProperty('jms_password'))
connection.start()
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)
vars.putObject('stubConnection', connection)
vars.putObject('stubSession', session)
vars.putObject('stubConsumer', session.createConsumer(session.createQueue('bookshelf.loan.requests')))
vars.putObject('stubProducer', session.createProducer(null))
}
def request = vars.getObject('stubConsumer').receive(1000)
if (request == null) {
return 'idle: no request within 1 s'
}
def reply = session.createTextMessage('{"loanId":"' + UUID.randomUUID() + '","status":"ACTIVE"}')
reply.setJMSCorrelationID(request.getJMSMessageID()) // JMS Message ID pattern
vars.getObject('stubProducer').send(request.getJMSReplyTo(), reply)
return 'replied to ' + request.getJMSMessageID()
# Terminal 1: the stub worker, until stopped with Ctrl+C
jmeter -n -t loan-worker-stub.jmx -q messaging.properties -l stub.jtl
# Terminal 2: the load test
jmeter -n -t bookshelf-jms-loans.jmx -q messaging.properties -Jthreads=20 -Jrampup=20 -Jduration=300 -l jms-loans.jtl
The stub uses the plain javax.jms API of the ActiveMQ client that is already in lib; JSR223 caching and the
vars and props bindings are explained on
Scripting with JSR223 & Groovy.
Temporary reply queues
Leaving JNDI name Receive queue empty switches Request Response to temporary queues: JMeter sets JMSReplyTo to a
temporary queue and the sending thread blocks until the reply arrives. The component reference stresses that this is
very different from a fixed reply queue. The service under test sees the same JMSReplyTo contract, so it needs no
change, but the broker now creates and deletes temporary destinations, which is extra load that production may not
have. Use the same reply mechanism as the real clients.
Read, Browse and Clear
The three utility styles make queue housekeeping part of the plan rather than a manual step:
-
a Clear sample in a setUp Thread Group empties
bookshelf.loan.requestsbefore the run, so leftover messages from a previous run do not distort the first minutes; -
a Browse sample in a tearDown Thread Group records the final queue depth (response message
N messages available on the queue), and an assertion can fail the run when it is not zero; -
a Read sample takes messages from a queue nobody consumes, for example to check what a Request Only run left behind.
JMS Publisher and JMS Subscriber
The JMS Publisher publishes messages to a destination (topic or queue) and the JMS Subscriber subscribes to messages from one. They are independent samplers, usually in different Thread Groups, as in the topic tutorial: one group of subscribers and one group of publishers.
JMS Publisher fields
| Field | JMX property (5.6.3 source) | Meaning |
|---|---|---|
Use jndi.properties file |
|
Use |
Initial Context Factory |
|
JNDI context factory class. |
Provider URL |
|
URL of the JMS provider. Required unless |
Connection Factory |
|
JNDI name of the connection factory ( |
Destination |
|
Topic or queue name, for example |
Setup |
|
At startup: the destination name is resolved once and stays the same. Each sample: it is evaluated at each sample, so it can contain variables. |
Use Authorization? / User / Password |
|
Whether the provider needs authentication, and the credentials (stored unencrypted in the plan). |
Expiration (ms) |
|
Message time to live; default |
Priority (0-9) |
|
0 (lowest) to 9 (highest); default 4. |
Reconnect on error codes (regex) |
|
Regular expression of |
Number of samples to aggregate |
|
How many messages one sample publishes. Required. |
Message source |
|
From file ( |
Message Type |
|
Text Message, Map Message, Object Message or Bytes Message ( |
Content encoding |
|
How a source file is read: RAW (no variable support, default system charset), DEFAULT (default system encoding, XML prolog respected, variables processed) or a named charset. |
Use non-persistent delivery mode? |
|
Sends with |
JMS Properties |
|
Message properties with name, value and class of value (String by default). |
The message sources behave as follows: From file reads the file once and reuses it for all samples (since JMeter 3.0
it is reloaded when the file name changes, so the name can contain a variable); Random File from folder picks a random
file each sample, and the folder must contain .dat files for bytes messages or .txt / .obj files for text and
object messages; Textarea takes the message from the text area. Since JMeter 3.2, variables are supported for every
message type and source.
A map message is read as lines of text, each with three comma-separated fields: entry name, class name (String
assumes java.lang) and a string value converted with that class’s valueOf(String):
loanId,String,7f3c2a10-0000-4000-8000-000000000001
bookId,Integer,42
borrower,String,reader-7
An object message needs the jar of the object’s class and its dependencies in lib, and the object serialized as
XML with XStream, either in a .txt / .obj file or in the text area; property replacement happens in the text area
but not in a file.
JMS Subscriber fields
| Field | JMX property (5.6.3 source) | Meaning |
|---|---|---|
Use jndi.properties file, Initial Context Factory, Provider URL, Connection Factory |
as for the Publisher |
JNDI setup; the Provider URL is optional here. |
Destination |
|
Topic or queue name. Required. |
Durable Subscription ID |
|
ID of a durable subscription; the provider creates it on first use if it does not exist yet. |
Client ID |
|
Client ID used with a durable subscription. With more than one thread, include a variable such as
|
JMS Selector |
|
Message selector (SQL-92 subset) to receive only matching messages. |
Setup |
|
At startup or Each sample, as for the Publisher. |
Use Authorization? / User / Password |
|
As for the Publisher. |
Number of samples to aggregate |
|
How many messages one sample must receive. Required. |
Store Response (component reference: "Save response") |
|
Store the message content in the result; if not, only the response length is recorded. |
Timeout (ms) |
|
Overall timeout for the whole sample, not per message; |
Client |
|
Use MessageConsumer.receive() ( |
Stop between samples? |
|
If selected, |
Separator |
|
Separator between aggregated messages in the response ( |
Reconnect on error codes (regex) |
|
As for the Publisher. |
Pause between errors (ms) |
|
Pause after an error before trying again. |
Since JMeter 3.0 a Subscriber sample is an error when the number of messages received differs from Number of samples to aggregate; before 3.0, one message was enough for success. The Timeout therefore has to cover the time the whole batch needs to arrive at the expected rate.
Source: JMS Subscriber (Component Reference) and Changes history
Example: Bookshelf loan events fan-out
When a loan is created, the Bookshelf platform publishes a LoanCreated event on the bookshelf.loan.events topic;
the notification, analytics, recommendation, audit and search-index services each subscribe. The test checks that the
broker sustains the publish rate and delivers every event to every subscriber, mirroring the topic tutorial: five
subscribers and one publisher.
-
Add → Threads (Users) → Thread Group named
Subscribers: 5 threads, ramp-up 0, Infinite loops, Specify Thread lifetime with Duration${__P(duration,120)}. -
Add → Threads (Users) → Thread Group named
Publisher: 1 thread, ramp-up 0, Infinite loops, Duration${__P(duration,120)}and Startup delay5, so the non-durable subscriptions exist before the first event (a non-durable subscriber never sees messages published before it subscribed). -
Under
Subscribers, Add → Sampler → JMS Subscriber: Initial Context Factory and Provider URL as before, Connection FactoryConnectionFactory, DestinationdynamicTopics/bookshelf.loan.events, Setup At startup, Use Authorization? Required with${__P(jms_user,)}and${__P(jms_password,)}, Number of samples to aggregate10, Store Response cleared, Timeout (ms)10000, Client Use MessageListener.onMessage(). -
Under
Publisher, Add → Sampler → JMS Publisher: the same connection fields, Number of samples to aggregate1, Message source Textarea, Message Type Text Message, and the event body below; then Add → Timer → Constant Timer of 200 ms under it.
<SubscriberSampler guiclass="JMSSubscriberGui" testclass="SubscriberSampler" testname="Loan events subscriber" enabled="true">
<stringProp name="jms.jndi_properties">false</stringProp>
<stringProp name="jms.initial_context_factory">org.apache.activemq.jndi.ActiveMQInitialContextFactory</stringProp>
<stringProp name="jms.provider_url">${__P(jms_url,tcp://localhost:61616)}</stringProp>
<stringProp name="jms.connection_factory">ConnectionFactory</stringProp>
<stringProp name="jms.topic">dynamicTopics/bookshelf.loan.events</stringProp>
<boolProp name="jms.authenticate">true</boolProp>
<stringProp name="jms.security_principle">${__P(jms_user,)}</stringProp>
<stringProp name="jms.security_credentials">${__P(jms_password,)}</stringProp>
<stringProp name="jms.iterations">10</stringProp>
<stringProp name="jms.read_response">false</stringProp>
<stringProp name="jms.timeout">10000</stringProp>
<stringProp name="jms.client_choice">jms_subscriber_on_message</stringProp>
<stringProp name="jms_error_reconnect_on_codes"></stringProp>
</SubscriberSampler>
<hashTree/>
<PublisherSampler guiclass="JMSPublisherGui" testclass="PublisherSampler" testname="Loan events publisher" enabled="true">
<stringProp name="jms.jndi_properties">false</stringProp>
<stringProp name="jms.initial_context_factory">org.apache.activemq.jndi.ActiveMQInitialContextFactory</stringProp>
<stringProp name="jms.provider_url">${__P(jms_url,tcp://localhost:61616)}</stringProp>
<stringProp name="jms.connection_factory">ConnectionFactory</stringProp>
<stringProp name="jms.topic">dynamicTopics/bookshelf.loan.events</stringProp>
<boolProp name="jms.authenticate">true</boolProp>
<stringProp name="jms.security_principle">${__P(jms_user,)}</stringProp>
<stringProp name="jms.security_credentials">${__P(jms_password,)}</stringProp>
<stringProp name="jms.iterations">1</stringProp>
<stringProp name="jms.config_choice">jms_use_text</stringProp>
<stringProp name="jms.config_msg_type">jms_text_message</stringProp>
<stringProp name="jms.text_message">{"type":"LoanCreated","loanId":"${__UUID()}","bookId":${__Random(1,500)}}</stringProp>
<stringProp name="jms.expiration"></stringProp>
<stringProp name="jms.priority"></stringProp>
<stringProp name="jms_error_reconnect_on_codes"></stringProp>
<elementProp name="jms.jmsProperties" elementType="JMSProperties">
<collectionProp name="JMSProperties.properties">
<elementProp name="" elementType="JMSProperty">
<stringProp name="JMSProperty.name">eventType</stringProp>
<stringProp name="JMSProperty.value">LoanCreated</stringProp>
<stringProp name="JMSProperty.type">java.lang.String</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</PublisherSampler>
<hashTree>
<ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Event interval" enabled="true">
<stringProp name="ConstantTimer.delay">200</stringProp>
</ConstantTimer>
<hashTree/>
</hashTree>
Worked numbers. The publisher waits 200 ms and then spends a few milliseconds publishing, so it sends a little under 1000 / 200 = 5 events per second. Each of the 5 subscribers receives all of them, so the broker makes about 5 × 5 = 25 deliveries per second. A subscriber sample needs 10 events, so it completes about every 10 / 5 = 2 s, and the 10 s timeout leaves five times that margin; if the subscriber samples start failing with fewer than 10 messages, events are being lost or delayed, not merely slow. Over a 120 s run, minus the 5 s publisher delay, expect about 115 × 5 ≈ 575 events published and about 57 successful samples per subscriber. The tutorial’s own arithmetic is simpler (6 threads × 1 message × 10 iterations = 60 messages), because it only loops a fixed number of times.
The eventType property lets a subscriber use a JMS Selector: eventType = 'LoanCreated' makes, for example,
the search-index subscriber receive only the events it cares about. A durable subscription (a Durable Subscription
ID plus a Client ID such as search-index-${__threadNum}) keeps events for a subscriber that is temporarily
disconnected, which is how to test catch-up after a consumer restart.
Why messages are aggregated
The topic tutorial explains Number of samples to aggregate by performance: small messages arrive very quickly, and if the sampler did not aggregate them JMeter would not be able to keep up. Each sample costs a result object, listener work and a line in the JTL file; at thousands of messages per second, one sample per message turns the load generator into the bottleneck. Aggregating 10 or 100 messages per sample divides that overhead accordingly. The flip side is that one sample’s elapsed time covers the whole batch, so per-message latency is not visible; when it matters, put a timestamp in the message body and compute it on the consumer side, or use request/reply.
The tutorial also notes that the two subscriber client implementations exist because some providers show zombie threads with one of them: if that happens, try the other.
JMS on the load generator and common pitfalls
-
Connections per thread. The JMS samplers open their connections per thread, so 200 threads mean about 200 broker connections, each with its sessions and, for subscribers, buffers of prefetched messages. That load on the broker is part of the test; on the generator it costs memory and threads.
-
One shared reply matcher. With a fixed reply queue, the 5.6.3 source matches every reply through one shared, synchronized registry; at very high request rates that becomes a point of contention inside JMeter, which shows up as a rising round trip that the broker’s own metrics do not confirm. Spread such load over more JMeter instances.
-
Stored responses. Store Response on a subscriber, and View Results Tree anywhere, keep message bodies in memory. Clear it for load runs.
-
Object messages. XStream serialization and deserialization cost CPU per message; prefer text or bytes messages unless the system really uses object messages.
-
Persistent versus non-persistent. Persistent delivery is the default and costs the broker a store write per message, so it is usually noticeably slower. Match production, and state the mode in the test report.
-
Leftover messages. A queue that still holds messages from an earlier run makes the first minutes of a request/response test meaningless (replies to old requests, a worker busy with stale work). Clear queues in a setUp Thread Group.
-
Timeouts that hide failures. A Point-to-Point Timeout of
0waits forever: a lost reply freezes the thread and silently lowers the offered load. Always set a finite timeout above the SLA. -
Wrong jar generation.
NoClassDefFoundErrorfor ajavax.jmsorjakarta.jmsclass injmeter.logalmost always means a client jar of the other API generation (see the table above).
SMTP Sampler
The SMTP Sampler sends mail messages using SMTP or SMTPS. It supports SSL and StartTLS, user authentication, CC, BCC
and Reply-To addresses, extra headers, attachments, and sending a ready-made .eml file. Every sample is a complete
SMTP session: the 5.6.3 source connects, authenticates if asked, sends one message and closes the connection. Its
purpose in a load test is to load a mail server, typically the relay the application sends through; to test how the
Bookshelf API behaves when it sends confirmation e-mails, load the API and point it at a test mail server instead.
Load tests must never send mail through a third-party or production mail service: besides the risk of spamming real addresses, providers rate-limit and block such traffic. Use a local test server such as GreenMail.
Every field
The JMX property names are from the 5.6.3 source (SmtpSampler.java and SecuritySettingsPanel.java).
| Field | JMX property | Meaning |
|---|---|---|
Server |
|
Host name or IP address of the server. Required. |
Port |
|
Defaults: SMTP 25, SSL 465, StartTLS 587. |
Connection timeout |
|
Socket connection timeout in ms; default infinite. |
Read timeout |
|
Socket read timeout in ms; default infinite. |
Address From |
|
Sender address. Required. |
Address To |
|
Recipients, separated by |
Address To CC / Address To BCC |
|
Carbon-copy and blind-carbon-copy recipients, separated by |
Address Reply-To |
|
Alternate Reply-To addresses, separated by |
Use Auth / Username / Password |
|
Whether the server requires authentication, and the credentials (stored unencrypted in the plan). |
Use no security features / Use SSL / Use StartTLS |
|
Plain connection, SSL from the first byte, or an upgrade with StartTLS. |
Enforce StartTLS |
|
Terminate the connection if the server does not start TLS. |
Trust All Certificates |
|
Accept any certificate, whatever its CA (skips certificate chain verification). |
Use local truststore / Local truststore |
|
Validate the chain against a local truststore file; relative paths are resolved against the current directory, then
the directory of the |
Override System SSL/TLS Protocols |
|
Space-separated list of protocols for the handshake, for example |
Subject |
|
Message subject. |
Suppress Subject Header |
|
Omit the |
Include timestamp in subject |
|
Append |
Add Header |
|
Extra headers, name and value. |
Message |
|
Message body. |
Send plain body (i.e. not multipart/mixed) |
|
Send the body as a plain message when possible; with an empty body and one file, the file becomes the body. With a non-empty body and at least one attachment, the message is multipart/mixed regardless. |
Attach file(s) |
|
Files to attach, separated by |
Send .eml |
|
Send the given |
Calculate message size |
|
Compute the message size and store it in the sample result (the bytes column); otherwise it is not computed. |
Enable debug logging? |
|
Sets the JavaMail |
A successful sample has response code 200 and the response message Message successfully sent!; failures get
response code 500 with the exception text, and a wrong user name or password shows up as
AuthenticationFailedException.
Example: loan confirmation mails into GreenMail
-
Add → Threads (Users) → Thread Group named
Mail senders:${__P(threads,1)}threads, ramp-up${__P(rampup,1)}, Infinite loops, Duration${__P(duration,60)}. -
Add → Sampler → SMTP Sampler: Server
localhost, Port3025, Connection timeout5000, Read timeout10000, Address Fromnoreply@bookshelf.test, Address Toreader1@bookshelf.test, Use no security features (GreenMail’s plain port), SubjectLoan confirmation ${__threadNum}with Include timestamp in subject, a short Message, and Calculate message size selected. -
Add → Timer → Constant Timer of 2000 ms under the sampler.
<SmtpSampler guiclass="SmtpSamplerGui" testclass="SmtpSampler" testname="Send loan confirmation" enabled="true">
<stringProp name="SMTPSampler.server">localhost</stringProp>
<stringProp name="SMTPSampler.serverPort">3025</stringProp>
<stringProp name="SMTPSampler.serverConnectionTimeout">5000</stringProp>
<stringProp name="SMTPSampler.serverTimeout">10000</stringProp>
<stringProp name="SMTPSampler.mailFrom">noreply@bookshelf.test</stringProp>
<stringProp name="SMTPSampler.receiverTo">reader1@bookshelf.test</stringProp>
<stringProp name="SMTPSampler.subject">Loan confirmation ${__threadNum}</stringProp>
<boolProp name="SMTPSampler.include_timestamp">true</boolProp>
<stringProp name="SMTPSampler.message">Your loan of book ${__Random(1,500)} is confirmed.</stringProp>
<boolProp name="SMTPSampler.messageSizeStatistics">true</boolProp>
<boolProp name="SMTPSampler.useAuth">false</boolProp>
<boolProp name="SMTPSampler.useSSL">false</boolProp>
<boolProp name="SMTPSampler.useStartTLS">false</boolProp>
<boolProp name="SMTPSampler.enableDebug">false</boolProp>
</SmtpSampler>
Worked numbers. With 10 threads and a 2 s pause, each cycle is the 2 s pause plus a few milliseconds of SMTP session, so the plan sends just under 10 / 2 = 5 messages per second, about 300 per minute, each as its own TCP connection: 5 connects, EHLO exchanges and QUITs per second on top of the message transfer. Against a relay that is configured for 50 messages per second, a ramp from 5 to 50 messages per second (10 to 100 threads at the same pause) answers whether the relay keeps up with the peak loan rate.
Mail Reader Sampler
The Mail Reader Sampler reads (and optionally deletes) mail messages using POP3(S) or IMAP(S). Each sample connects, logs in, opens the folder, retrieves the requested messages and closes the connection. The retrieved messages are stored as sub-samples of the main sample, and the parts of a multipart message as sub-samples of that message; since JMeter 2.12 the sample counts as one sample in listeners, not one per message.
| Field | JMX property (5.6.3 source) | Meaning |
|---|---|---|
Protocol (e.g. pop3, imaps) (component reference: "Server Type") |
|
Provider name: |
Server Host |
|
Host name or IP address. Required. |
Server Port (optional) |
|
Port; empty means the protocol’s default. |
Username |
|
Login name. |
Password |
|
Login password (stored unencrypted in the plan). |
Folder |
|
IMAP(S) folder, for example |
Number of messages to retrieve |
|
All (stored as |
Fetch headers only |
|
Retrieve only the message headers. |
Delete messages from the server |
|
Delete the messages after retrieval. |
Store the message using MIME (raw) |
|
Store the whole raw message in the response data (headers included); otherwise headers are stored as response headers and a few (Date, To, From, Subject) in the body. |
Use no security features / Use SSL / Use StartTLS / Enforce StartTLS / Trust All Certificates / Use local truststore / Local truststore |
|
Same security panel and meaning as in the SMTP Sampler (the source reuses its property names). |
Any JavaMail property can be passed to the sampler by adding it to user.properties (since JMeter 5.1, custom
mail.* properties are passed to the Mail Reader Sampler), for example IMAP timeouts:
# user.properties -- JavaMail properties for the Mail Reader Sampler
mail.imap.connectiontimeout=5000
mail.imap.timeout=10000
The file protocol reads raw messages from files: the Server field is the path to the parent of the folder, with
messages stored as n.msg files, or the name of a single-message file. The component reference describes it as basic
and mainly intended for debugging. The SMIME Assertion can check signed messages read by this sampler.
Example: an SMTP and IMAP round trip
A second Thread Group, Mailbox reader, with 1 thread, Infinite loops and the same Duration, reads the mailbox the
senders fill:
-
Add → Sampler → Mail Reader Sampler: Protocol
imap, Server Hostlocalhost, Server Port3143, Username${__P(mail_user,)}, Password${__P(mail_password,)}, FolderINBOX, Number of messages to retrieve All, Fetch headers only selected, Delete messages from the server selected, Use no security features. -
Add → Timer → Constant Timer of 10000 ms under it.
<MailReaderSampler guiclass="MailReaderSamplerGui" testclass="MailReaderSampler" testname="Read INBOX" enabled="true">
<stringProp name="host_type">imap</stringProp>
<stringProp name="host">localhost</stringProp>
<stringProp name="port">3143</stringProp>
<stringProp name="username">${__P(mail_user,)}</stringProp>
<stringProp name="password">${__P(mail_password,)}</stringProp>
<stringProp name="folder">INBOX</stringProp>
<intProp name="num_messages">-1</intProp>
<boolProp name="headerOnly">true</boolProp>
<boolProp name="delete">true</boolProp>
<boolProp name="storeMimeMessage">false</boolProp>
</MailReaderSampler>
jmeter -n -t bookshelf-mail.jmx -q messaging.properties -Jthreads=10 -Jrampup=10 -Jduration=60 -l mail.jtl
Creating summariser <summary>
Created the tree successfully using bookshelf-mail.jmx
Starting standalone test @ 2026 Oct 14 11:10:00 CEST (1791969000000)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 132 in 00:00:29 = 4.6/s Avg: 14 Min: 4 Max: 181 Err: 0 (0.00%) Active: 11 Started: 11 Finished: 0
summary + 154 in 00:00:31 = 5.0/s Avg: 12 Min: 4 Max: 63 Err: 0 (0.00%) Active: 0 Started: 11 Finished: 11
summary = 286 in 00:01:00 = 4.8/s Avg: 13 Min: 4 Max: 181 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 11:11:00 CEST (1791969060000)
... end of run
Worked numbers. At about 5 messages per second and one read every 10 s, each read finds about 50 messages, fetches their headers and deletes them, so the mailbox stays small. The summariser counts each read as one sample (the 280 SMTP samples plus 6 reads ≈ 286), while the messages appear as sub-samples in the JTL file when sub-results are saved. Reading All with full bodies, without deletion, from a mailbox that only grows would make every read slower than the last and measure the mailbox size rather than the server. The numbers are illustrative.
TCP Sampler
The TCP Sampler opens a TCP/IP connection to a server, sends text and waits for a response. It speaks no protocol of its own: the bytes it sends are exactly the Text to send (after variable substitution), and a pluggable TCPClient class decides how the text is encoded and, crucially, how the end of the response is detected. That makes it the sampler for line-based legacy services, simple binary protocols and anything else without a dedicated sampler.
Connections
-
With Re-use connection selected, connections are shared between TCP Samplers of the same thread that use exactly the same host name string and port. Different host/port combinations and different threads use different connections.
-
With both Re-use connection and Close connection selected, the socket is closed after the sampler runs and a new one is created by the next sampler, which is a way to close the socket at the end of each thread loop.
-
When an error is detected, or Re-use connection is not selected, the socket is closed and another one is opened on the next sample.
-
Sockets are disconnected at the end of the test run.
The client implementations
The class is chosen by the TCPClient classname field, failing that by the tcp.handler property; a short name is
looked up in the package org.apache.jmeter.protocol.tcp.sampler. Users can provide their own class, which must extend
org.apache.jmeter.protocol.tcp.sampler.TCPClient.
| Class | Request | End of the response |
|---|---|---|
|
The text, encoded with |
The end-of-line byte, if |
|
The text must be a hex-encoded string, converted to binary (and the response converted back to hex). |
The end-of-message byte, if |
|
As |
The number of bytes given by the response’s length prefix; the prefix is 2 bytes unless
|
The most common TCP Sampler bug follows from that table: with TCPClientImpl, no EOL byte and a server that keeps
the connection open, "end of the input stream" never comes, so every sample waits for the Response Timeout (or
forever, if none is set). Define how a response ends: an EOL byte, an EOM byte, a length prefix, or a server that
closes the connection. And if an EOL or EOM byte is used, the Response Timeout must be long enough, because the read
is cut off when it expires.
Properties
| Property | Default | Meaning |
|---|---|---|
|
|
Client class when the GUI field is empty. |
|
|
End-of-line byte for |
|
platform default |
Charset of |
|
not set |
End-of-message byte for |
|
|
Length-prefix size, in bytes, for |
|
not set |
Text before and after a status code in the response. |
|
not set |
Properties file that maps status codes to response messages. |
When tcp.status.prefix is defined, the response is searched for the text between prefix and suffix, which becomes the
response code, and the message comes from the properties file if given. With prefix [ and suffix ], the response
[J28] XI123,23,GBP,CR has response code J28. Response codes from 400 to 499 and 500 to 599 count as failures;
everything else is a success.
Every field
The JMX property names are from the 5.6.3 source (TCPSampler.java).
| Field | JMX property | Meaning |
|---|---|---|
TCPClient classname |
|
Client class; defaults to |
ServerName or IP |
|
TCP server. Required. |
Port Number |
|
Port. Required. |
Re-use connection |
|
Keep the connection open; otherwise it is closed when the data has been read. |
Close connection |
|
Close the connection after running the sampler. |
SO_LINGER |
|
Enable SO_LINGER with this linger time in seconds when a socket is created. |
End of line(EOL) byte value |
|
EOL byte; outside -128 to +127 skips EOL checking. Overrides |
Connect Timeout |
|
Connect timeout in ms; |
Response Timeout |
|
Response timeout in ms; |
Set NoDelay |
|
|
Text to send |
|
The text to send; functions and variables are evaluated. |
Login User / Password |
|
Not used by the supplied implementations (the password is stored unencrypted). |
TCP Sampler Config
Add → Config Element → TCP Sampler Config provides default data for the TCP Samplers in its scope, exactly like HTTP
Request Defaults for HTTP: TCPClient classname, ServerName or IP, Port Number, Re-use connection, Close connection,
SO_LINGER, End of line(EOL) byte value, Connect Timeout, Response Timeout, Set Nodelay and Text to send, with the same
JMX property names (it is stored as a ConfigTestElement with guiclass="TCPConfigGui"). A TCP Sampler leaves a field
empty to inherit the value from the config. An EOL byte set in TCP Sampler Config takes precedence over tcp.eolByte.
Example 1: a line-based stock service
The legacy Bookshelf stock service answers one line per request: STOCK <bookId> followed by a newline. Locally, a
socat echo server stands in for it (it returns each line unchanged, which is enough to exercise the sampler):
# A local echo server on 127.0.0.1:7000, one cat process per connection, until Ctrl+C
socat TCP-LISTEN:7000,bind=127.0.0.1,reuseaddr,fork EXEC:cat
-
Add → Config Element → TCP Sampler Config on the Thread Group: TCPClient classname
TCPClientImpl, ServerName or IP${__P(tcp_host,localhost)}, Port Number${__P(tcp_port,7000)}, Re-use connection selected, End of line(EOL) byte value10, Connect Timeout3000, Response Timeout5000. -
Add → Sampler → TCP Sampler with only Text to send:
STOCK ${__Random(1,500)}${__char(10)}(the__charfunction appends the newline byte). -
Add → Assertions → Response Assertion: Text Response Starts with
STOCK. -
Optionally, in
user.properties,tcp.charset=UTF-8so the encoding does not depend on the machine.
<ConfigTestElement guiclass="TCPConfigGui" testclass="ConfigTestElement" testname="TCP Sampler Config" enabled="true">
<stringProp name="TCPSampler.classname">TCPClientImpl</stringProp>
<stringProp name="TCPSampler.server">${__P(tcp_host,localhost)}</stringProp>
<stringProp name="TCPSampler.port">${__P(tcp_port,7000)}</stringProp>
<boolProp name="TCPSampler.reUseConnection">true</boolProp>
<boolProp name="TCPSampler.closeConnection">false</boolProp>
<boolProp name="TCPSampler.nodelay">false</boolProp>
<stringProp name="TCPSampler.EolByte">10</stringProp>
<stringProp name="TCPSampler.ctimeout">3000</stringProp>
<stringProp name="TCPSampler.timeout">5000</stringProp>
<stringProp name="TCPSampler.request"></stringProp>
</ConfigTestElement>
<hashTree/>
<TCPSampler guiclass="TCPSamplerGui" testclass="TCPSampler" testname="Stock lookup" enabled="true">
<stringProp name="TCPSampler.classname"></stringProp>
<stringProp name="TCPSampler.server"></stringProp>
<stringProp name="TCPSampler.port"></stringProp>
<boolProp name="TCPSampler.reUseConnection">true</boolProp>
<boolProp name="TCPSampler.closeConnection">false</boolProp>
<boolProp name="TCPSampler.nodelay">false</boolProp>
<stringProp name="TCPSampler.timeout"></stringProp>
<stringProp name="TCPSampler.request">STOCK ${__Random(1,500)}${__char(10)}</stringProp>
<stringProp name="ConfigTestElement.username"></stringProp>
<stringProp name="ConfigTestElement.password"></stringProp>
</TCPSampler>
Without the EOL byte, each sample of this plan would wait the full 5 s Response Timeout and fail, because cat
keeps the connection open. The sampler records connect time (since JMeter 3.0) and latency (since 3.3) as well as the
elapsed time, so the JTL file separates connection set-up from the service’s answer.
Source: TCP Sampler (Component Reference) and __char (Functions)
Example 2: a length-prefixed binary message
Binary protocols often frame messages with a length prefix. LengthPrefixedBinaryTCPClientImpl adds the 2-byte prefix
to the request and reads exactly as many bytes as the response’s prefix announces, so it works on a connection that
stays open. Against the same echo server, the response is the request itself:
-
TCPClient classname:
LengthPrefixedBinaryTCPClientImpl -
Text to send:
0102002A, the hex encoding of 4 bytes (for example a message type01, a version02and a book ID0x002A= 42) -
Response Assertion: Text Response Equals
0102002A
On the wire JMeter sends 0004 0102002A (the prefix 4 in two bytes, then the data) and expects the same framing back.
If the real protocol uses a 4-byte prefix, set tcp.binarylength.prefix.length=4. With plain BinaryTCPClientImpl,
the same request would need tcp.BinaryTCPClient.eomByte or a server that closes the connection.
Connection reuse, TIME_WAIT and the load generator
The TCP Sampler is light on CPU; its cost is sockets. Worked numbers for the stock lookup with 50 threads, a 1 s think time and a 5 ms response:
-
each thread makes about 1 / 1.005 ≈ 0.995 requests per second, so the plan offers ≈ 50 requests per second;
-
with Re-use connection, that is 50 long-lived sockets in total, one per thread;
-
without it, every sample opens and closes a connection: 50 new connections per second. The side that closes first keeps each closed socket in
TIME_WAIT, on Linux for 60 s, so about 50 × 60 = 3000 sockets sit inTIME_WAITon the load generator. At 500 requests per second that becomes 30 000, above the 28 232 ephemeral ports of the usual Linux default range (32768 to 60999), and new connections start failing with errors that look like a server problem.
Reuse connections when the real clients do, and close them per sample only when the real clients do too. SO_LINGER
0 makes the close abortive and avoids TIME_WAIT, as the component reference notes, at the price of a reset instead
of an orderly close; use it only when the server tolerates that. Set NoDelay matters for small request/response
messages: with Nagle’s algorithm on, a small write can be delayed waiting for an acknowledgement, which inflates
response times for reasons that have nothing to do with the server.
Running the messaging plans in CLI mode
All the plans on this page run like any other in CLI mode, with the properties file for the connection details. Results go to JTL files and the HTML dashboard as usual; those are covered on the later pages about running tests in CLI mode, listeners and the dashboard. A combined run of the three Bookshelf messaging plans against the local test bed:
docker compose up -d
socat TCP-LISTEN:7000,bind=127.0.0.1,reuseaddr,fork EXEC:cat &
jmeter -n -t bookshelf-jms-loans.jmx -q messaging.properties -Jthreads=20 -Jrampup=20 -Jduration=300 -l jms-loans.jtl
jmeter -n -t bookshelf-mail.jmx -q messaging.properties -Jthreads=10 -Jrampup=10 -Jduration=300 -l mail.jtl
jmeter -n -t bookshelf-tcp.jmx -q messaging.properties -Jthreads=50 -Jrampup=50 -Jduration=300 -l tcp.jtl
The usual sizing rules for the load generator apply unchanged (CLI mode, no View Results Tree, few listeners, results to CSV); the messaging-specific parts are the connection counts, aggregation and stored message bodies described above.
What changed: 2.x and 3.0 → 5.6 → 6.0
The books on the market target JMeter 2.11 to 3.0. The samplers on this page have kept their shape since then; what changed is mostly behaviour and options:
| Version | Change |
|---|---|
2.11 |
Typed values (class of value) for JMS properties in JMS Publisher and JMS Point-to-Point. |
2.12 |
JMS Priority and Expiration fields; SMTP Sampler connection and read timeouts; the Mail Reader Sampler counts as one sample instead of one per message. |
3.0 |
JMS Subscriber fails a sample when it receives fewer messages than expected (before, one message was enough); JMS Publisher reloads a From file source when the file name changes; TCP Sampler records connect time; the SMTP Sampler enables all protocols enabled on the default SSL context. |
3.2 |
JMS Publisher and Subscriber: reconnect on error codes and pause between errors; variables supported in all JMS message types and sources, with a content encoding. |
3.3 |
TCP Sampler measures latency. |
4.0 |
JMS Point-to-Point gains the Read, Browse and Clear styles; a timeout of |
5.0 |
SMTP Sampler: Override System SSL/TLS Protocols. |
5.1 |
Custom |
5.3 |
Reworked JMS Publisher and JMS Subscriber GUIs. |
5.4 |
|
5.5 |
SMTP Sampler encodes the personal part of e-mail addresses. |
5.6.3 |
Current release; JMS samplers compiled against |
6.0 (planned) |
Java 17+; the master-branch JMS module uses |
Source: Changes history, Changes and Apache JMeter source
References
-
JMS Point-to-Point, JMS Publisher and JMS Subscriber (Component Reference)
-
SMTP Sampler, Mail Reader Sampler and SMIME Assertion (Component Reference)
-
TCP Sampler and TCP Sampler Config (Component Reference)
-
Getting Started (User Manual): optional libraries (JMS client, ActiveMQ), classpath, CLI mode
-
Properties Reference (
tcp.*,JMSSampler.useSecurity.properties) -
Functions (
__P,__Random,__UUID,__threadNum,__char) -
Apache JMeter source (5.6.3
JMSSampler,PublisherSampler,SubscriberSampler,SmtpSampler,MailReaderSampler,TCPSampler,messages.properties,bin/jmeter.propertiesand the build files; master branch for the plannedjakarta.jmsmove) -
ActiveMQ Classic: JNDI Support, ActiveMQ Classic download and ActiveMQ Classic Docker image