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.

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)

javax.jms (the Apache Geronimo JMS 1.1 spec jar shipped in lib)

Clients that implement javax.jms: ActiveMQ Classic 5.x (for example activemq-all-5.19.2.jar), the ActiveMQ Artemis "JMS" client (not the "Jakarta" one), IBM MQ’s javax.jms client, and so on.

6.0 (planned)

jakarta.jms (Jakarta Messaging 3.1) in the master-branch source

Clients that implement jakarta.jms: ActiveMQ Classic 6.x, the Artemis Jakarta client, and so on.

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.properties file (Publisher and Subscriber only, through the Use jndi.properties file checkbox): the file must be on the classpath, for example in a directory added to user.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

org.apache.activemq.jndi.ActiveMQInitialContextFactory

Provider URL

vm://localhost, or tcp://localhost:61616 for a separate broker

vm:// starts an embedded broker inside JMeter; the tutorials use tcp://localhost:61616.

Provider URL

vm:(broker:(vm://localhost)?persistent=false)

Embedded broker with persistence disabled

Queue reference

dynamicQueues/QUEUENAME

Defines the queue in JNDI dynamically, without configuration

Topic reference

dynamicTopics/TOPICNAME

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

0

Sends messages and does not monitor replies ("fire and forget"). Use it to load a broker or a consumer service.

Request Response

1

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 JMSReplyTo header.

Read

2

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

3

Returns the current queue depth without removing messages.

Clear

4

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 JMSMessageID is used, otherwise the request’s JMSCorrelationID (which then has to be set on the request, through the JMS Properties table).

  • Use Response Message Id: if selected, the response’s JMSMessageID is used, otherwise the response’s JMSCorrelationID.

Pattern Use Request Message Id Use Response Message Id The service must…​

JMS Message ID pattern (most common)

selected

cleared

copy the request’s JMSMessageID into the reply’s JMSCorrelationID.

JMS Correlation ID pattern

cleared

cleared

copy the request’s JMSCorrelationID into the reply’s JMSCorrelationID; the plan sets a unique correlation ID on each request.

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

sequenceDiagram participant T as JMeter thread participant RQ as bookshelf.loan.requests participant W as Loan worker participant RP as bookshelf.loan.replies T->>RQ: send request, JMSReplyTo = replies queue Note over T: message ID ID:42 recorded, timer started RQ->>W: deliver request W->>RP: reply with JMSCorrelationID = ID:42 RP->>T: shared receiver matches ID:42 to the waiting thread Note over T: sample ends, elapsed = round trip

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

testname attribute

Label in the tree and in results.

QueueConnection Factory

JMSSampler.queueconnectionfactory

JNDI name of the queue connection factory. Required. ConnectionFactory for ActiveMQ.

JNDI name Request queue

JMSSampler.SendQueue

JNDI name of the queue the messages are sent to. Required.

JNDI name Receive queue (component reference: "JNDI Name Reply queue")

JMSSampler.ReceiveQueue

JNDI name of the reply queue. Used by Request Response; empty means temporary queues.

Number of samples to aggregate

JMSSampler.jmsNumberOfSamplesToAggregate

Only for the Read style: how many messages one sample reads. Default 1.

JMS Selector

JMSSampler.jmsSelector

Message selector (the SQL-92 subset defined by JMS) to take only matching messages.

Communication style

JMSSampler.communicationStyle

Request Only, Request Response, Read, Browse or Clear (see above).

Use Request Message Id, Use Response Message Id

JMSSampler.useReqMsgIdAsCorrelId, JMSSampler.useResMsgIdAsCorrelId

Correlation fields (see above).

Timeout (ms)

JMSSampler.timeout

Reply timeout. Default 2000 ms; 0 means no timeout (since JMeter 4.0; earlier, 0 was silently replaced by 2000 ms).

Expiration (ms)

JMSSampler.expiration

Message time to live. Default 0, never expires.

Priority (0-9)

JMSSampler.priority

Message priority, from 0 (lowest) to 9 (highest). Default 4.

Use non-persistent delivery mode?

JMSSampler.isNonPersistent

Sends with DeliveryMode.NON_PERSISTENT.

Content

HTTPSamper.xml_data (sic)

The message body (a text message). JMeter functions and variables are evaluated.

JMS Properties

arguments (a JMSProperties element)

Message properties: name, value and Class of value (java.lang.String by default, or another class such as java.lang.Integer).

Initial Context Factory

JMSSampler.initialContextFactory

Class of the JNDI context factory.

JNDI Properties

JMSSampler.jndiProperties

Extra properties for the JNDI implementation, such as queue.<jndiName>=<physicalName> entries.

Provider URL

JMSSampler.contextProviderUrl

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.

  1. Copy the client jar into lib as shown above and start JMeter.

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

  3. 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,)} and java.naming.security.credentials = ${__P(jms_password,)}

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

ConnectionFactory

ActiveMQ’s default JNDI name

JNDI name Request queue

LoanRequests

Mapped by queue.LoanRequests below

JNDI name Receive queue

LoanReplies

Fixed reply queue, mapped by queue.LoanReplies

Communication style

Request Response

A worker must reply

Use Request Message Id / Use Response Message Id

selected / cleared

JMS Message ID pattern

Timeout (ms)

5000

Above the loan SLA, so slow replies are measured rather than dropped

Content

{"bookId":${__Random(1,500)},"borrower":"reader-${__threadNum}"}

One loan request per sample

JNDI Properties

queue.LoanRequests = bookshelf.loan.requests, queue.LoanReplies = bookshelf.loan.replies, plus the two java.naming.security.* entries

JNDI names to physical queues, and credentials

Provider URL

${__P(jms_url,tcp://localhost:61616)}

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.requests before 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

jms.jndi_properties

Use jndi.properties from the classpath instead of the next two fields.

Initial Context Factory

jms.initial_context_factory

JNDI context factory class.

Provider URL

jms.provider_url

URL of the JMS provider. Required unless jndi.properties is used.

Connection Factory

jms.connection_factory

JNDI name of the connection factory (ConnectionFactory for ActiveMQ). This field is in the GUI and in the topic tutorial, though the component reference table omits it.

Destination

jms.topic

Topic or queue name, for example dynamicTopics/bookshelf.loan.events or the JNDI name LoanEvents. Required.

Setup

jms.destination_static

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

jms.authenticate, jms.security_principle (sic), jms.security_credentials

Whether the provider needs authentication, and the credentials (stored unencrypted in the plan).

Expiration (ms)

jms.expiration

Message time to live; default 0, never expires.

Priority (0-9)

jms.priority

0 (lowest) to 9 (highest); default 4.

Reconnect on error codes (regex)

jms_error_reconnect_on_codes

Regular expression of JMSException error codes that force a reconnection; empty means never reconnect.

Number of samples to aggregate

jms.iterations

How many messages one sample publishes. Required.

Message source

jms.config_choice

From file (jms_use_file), Random File from folder specified below (jms_use_random_file) or Textarea (jms_use_text).

Message Type

jms.config_msg_type

Text Message, Map Message, Object Message or Bytes Message (jms_text_message, jms_map_message, jms_object_message, jms_bytes_message).

Content encoding

jms.file_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?

jms.non_persistent

Sends with DeliveryMode.NON_PERSISTENT; default false.

JMS Properties

jms.jmsProperties

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

jms.topic

Topic or queue name. Required.

Durable Subscription ID

jms.durableSubscriptionId

ID of a durable subscription; the provider creates it on first use if it does not exist yet.

Client ID

jms.clientId

Client ID used with a durable subscription. With more than one thread, include a variable such as ${__threadNum} so each thread has its own.

JMS Selector

jms.selector

Message selector (SQL-92 subset) to receive only matching messages.

Setup

jms.destination_static

At startup or Each sample, as for the Publisher.

Use Authorization? / User / Password

jms.authenticate, jms.security_principle, jms.security_credentials

As for the Publisher.

Number of samples to aggregate

jms.iterations

How many messages one sample must receive. Required.

Store Response (component reference: "Save response")

jms.read_response

Store the message content in the result; if not, only the response length is recorded.

Timeout (ms)

jms.timeout

Overall timeout for the whole sample, not per message; 0 means none.

Client

jms.client_choice

Use MessageConsumer.receive() (jms_subscriber_receive): calls receive() for every requested message, keeps the connection between samples but only fetches while the sampler is active; best for queues. Use MessageListener.onMessage() (jms_subscriber_on_message): a listener stores all incoming messages on an internal queue and stays active after the sample; best for topics.

Stop between samples?

jms.stop_between_samples

If selected, Connection.stop() is called after each sample and start() before the next; otherwise the connection is started once per thread and stopped at the end.

Separator

jms.separator

Separator between aggregated messages in the response (\n, \r and \t are accepted).

Reconnect on error codes (regex)

jms_error_reconnect_on_codes

As for the Publisher.

Pause between errors (ms)

jms_error_pause_between

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.

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.

flowchart LR subgraph PUB [Publisher Thread Group, 1 thread] P[JMS Publisher, 1 message per sample, Constant Timer 200 ms] end P -->|about 5 msg/s| T((bookshelf.loan.events)) subgraph SUB [Subscribers Thread Group, 5 threads] S1[JMS Subscriber 1] S2[JMS Subscriber 2] S3[JMS Subscriber 3] S4[JMS Subscriber 4] S5[JMS Subscriber 5] end T --> S1 T --> S2 T --> S3 T --> S4 T --> S5
  1. Add → Threads (Users) → Thread Group named Subscribers: 5 threads, ramp-up 0, Infinite loops, Specify Thread lifetime with Duration ${__P(duration,120)}.

  2. Add → Threads (Users) → Thread Group named Publisher: 1 thread, ramp-up 0, Infinite loops, Duration ${__P(duration,120)} and Startup delay 5, so the non-durable subscriptions exist before the first event (a non-durable subscriber never sees messages published before it subscribed).

  3. Under Subscribers, Add → Sampler → JMS Subscriber: Initial Context Factory and Provider URL as before, Connection Factory ConnectionFactory, Destination dynamicTopics/bookshelf.loan.events, Setup At startup, Use Authorization? Required with ${__P(jms_user,)} and ${__P(jms_password,)}, Number of samples to aggregate 10, Store Response cleared, Timeout (ms) 10000, Client Use MessageListener.onMessage().

  4. Under Publisher, Add → Sampler → JMS Publisher: the same connection fields, Number of samples to aggregate 1, 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 0 waits forever: a lost reply freezes the thread and silently lowers the offered load. Always set a finite timeout above the SLA.

  • Wrong jar generation. NoClassDefFoundError for a javax.jms or jakarta.jms class in jmeter.log almost 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

SMTPSampler.server

Host name or IP address of the server. Required.

Port

SMTPSampler.serverPort

Defaults: SMTP 25, SSL 465, StartTLS 587.

Connection timeout

SMTPSampler.serverConnectionTimeout

Socket connection timeout in ms; default infinite.

Read timeout

SMTPSampler.serverTimeout

Socket read timeout in ms; default infinite.

Address From

SMTPSampler.mailFrom

Sender address. Required.

Address To

SMTPSampler.receiverTo

Recipients, separated by ;. Required unless CC or BCC is given.

Address To CC / Address To BCC

SMTPSampler.receiverCC, SMTPSampler.receiverBCC

Carbon-copy and blind-carbon-copy recipients, separated by ;.

Address Reply-To

SMTPSampler.replyTo

Alternate Reply-To addresses, separated by ;.

Use Auth / Username / Password

SMTPSampler.useAuth, SMTPSampler.username, SMTPSampler.password

Whether the server requires authentication, and the credentials (stored unencrypted in the plan).

Use no security features / Use SSL / Use StartTLS

SMTPSampler.useSSL, SMTPSampler.useStartTLS (neither set means no security)

Plain connection, SSL from the first byte, or an upgrade with StartTLS.

Enforce StartTLS

SMTPSampler.enforceStartTLS

Terminate the connection if the server does not start TLS.

Trust All Certificates

SMTPSampler.trustAllCerts

Accept any certificate, whatever its CA (skips certificate chain verification).

Use local truststore / Local truststore

SMTPSampler.useLocalTrustStore, SMTPSampler.trustStoreToUse

Validate the chain against a local truststore file; relative paths are resolved against the current directory, then the directory of the .jmx file.

Override System SSL/TLS Protocols

SMTPSampler.tlsProtocols

Space-separated list of protocols for the handshake, for example TLSv1.2 TLSv1.3; default all supported (field added in JMeter 5.0).

Subject

SMTPSampler.subject

Message subject.

Suppress Subject Header

SMTPSampler.suppressSubject

Omit the Subject: header entirely (not the same as an empty subject).

Include timestamp in subject

SMTPSampler.include_timestamp

Append System.currentTimeMillis() to the subject.

Add Header

SMTPSampler.headerFields

Extra headers, name and value.

Message

SMTPSampler.message

Message body.

Send plain body (i.e. not multipart/mixed)

SMTPSampler.plainBody

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)

SMTPSampler.attachFile

Files to attach, separated by ;.

Send .eml

SMTPSampler.use_eml, SMTPSampler.emlMessageToSend

Send the given .eml file instead of the Subject, Message and attachment fields.

Calculate message size

SMTPSampler.messageSizeStatistics

Compute the message size and store it in the sample result (the bytes column); otherwise it is not computed.

Enable debug logging?

SMTPSampler.enableDebug

Sets the JavaMail mail.debug property to true. Never in a load run: it logs the whole SMTP dialogue.

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

  1. Add → Threads (Users) → Thread Group named Mail senders: ${__P(threads,1)} threads, ramp-up ${__P(rampup,1)}, Infinite loops, Duration ${__P(duration,60)}.

  2. Add → Sampler → SMTP Sampler: Server localhost, Port 3025, Connection timeout 5000, Read timeout 10000, Address From noreply@bookshelf.test, Address To reader1@bookshelf.test, Use no security features (GreenMail’s plain port), Subject Loan confirmation ${__threadNum} with Include timestamp in subject, a short Message, and Calculate message size selected.

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

host_type

Provider name: pop3, pop3s, imap, imaps, or another JavaMail provider such as file. Required.

Server Host

host

Host name or IP address. Required.

Server Port (optional)

port

Port; empty means the protocol’s default.

Username

username

Login name.

Password

password

Login password (stored unencrypted in the plan).

Folder

folder

IMAP(S) folder, for example INBOX. Required for IMAP(S).

Number of messages to retrieve

num_messages

All (stored as -1) or a number. Required.

Fetch headers only

headerOnly

Retrieve only the message headers.

Delete messages from the server

delete

Delete the messages after retrieval.

Store the message using MIME (raw)

storeMimeMessage

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

SMTPSampler.useSSL, SMTPSampler.useStartTLS, SMTPSampler.enforceStartTLS, SMTPSampler.trustAllCerts, SMTPSampler.useLocalTrustStore, SMTPSampler.trustStoreToUse

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:

  1. Add → Sampler → Mail Reader Sampler: Protocol imap, Server Host localhost, Server Port 3143, Username ${__P(mail_user,)}, Password ${__P(mail_password,)}, Folder INBOX, Number of messages to retrieve All, Fetch headers only selected, Delete messages from the server selected, Use no security features.

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

TCPClientImpl (default)

The text, encoded with tcp.charset (default: the platform’s default encoding).

The end-of-line byte, if tcp.eolByte (or the GUI field) is set; otherwise the end of the input stream.

BinaryTCPClientImpl

The text must be a hex-encoded string, converted to binary (and the response converted back to hex).

The end-of-message byte, if tcp.BinaryTCPClient.eomByte is set; otherwise the end of the input stream.

LengthPrefixedBinaryTCPClientImpl

As BinaryTCPClientImpl, prefixed with a binary length.

The number of bytes given by the response’s length prefix; the prefix is 2 bytes unless tcp.binarylength.prefix.length says otherwise.

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

tcp.handler

TCPClientImpl

Client class when the GUI field is empty.

tcp.eolByte

1000 (disabled)

End-of-line byte for TCPClientImpl; a value outside -128 to +127 disables EOL checking. The GUI field wins over the property.

tcp.charset

platform default

Charset of TCPClientImpl.

tcp.BinaryTCPClient.eomByte

not set

End-of-message byte for BinaryTCPClientImpl.

tcp.binarylength.prefix.length

2

Length-prefix size, in bytes, for LengthPrefixedBinaryTCPClientImpl.

tcp.status.prefix, tcp.status.suffix

not set

Text before and after a status code in the response.

tcp.status.properties

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

TCPSampler.classname

Client class; defaults to tcp.handler, failing that TCPClientImpl.

ServerName or IP

TCPSampler.server

TCP server. Required.

Port Number

TCPSampler.port

Port. Required.

Re-use connection

TCPSampler.reUseConnection

Keep the connection open; otherwise it is closed when the data has been read.

Close connection

TCPSampler.closeConnection

Close the connection after running the sampler.

SO_LINGER

TCPSampler.soLinger

Enable SO_LINGER with this linger time in seconds when a socket is created. 0 can prevent large numbers of sockets sitting in TIME_WAIT.

End of line(EOL) byte value

TCPSampler.EolByte

EOL byte; outside -128 to +127 skips EOL checking. Overrides tcp.eolByte.

Connect Timeout

TCPSampler.ctimeout

Connect timeout in ms; 0 disables.

Response Timeout

TCPSampler.timeout

Response timeout in ms; 0 disables.

Set NoDelay

TCPSampler.nodelay

Socket.setTcpNoDelay(): disables Nagle’s algorithm when selected.

Text to send

TCPSampler.request

The text to send; functions and variables are evaluated.

Login User / Password

ConfigTestElement.username, ConfigTestElement.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
  1. 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 value 10, Connect Timeout 3000, Response Timeout 5000.

  2. Add → Sampler → TCP Sampler with only Text to send: STOCK ${__Random(1,500)}${__char(10)} (the __char function appends the newline byte).

  3. Add → Assertions → Response Assertion: Text Response Starts with STOCK.

  4. Optionally, in user.properties, tcp.charset=UTF-8 so 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.

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 type 01, a version 02 and a book ID 0x002A = 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 in TIME_WAIT on 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 0 now means "no timeout" instead of silently using 2000 ms.

5.0

SMTP Sampler: Override System SSL/TLS Protocols.

5.1

Custom mail.* properties passed to the Mail Reader Sampler.

5.3

Reworked JMS Publisher and JMS Subscriber GUIs.

5.4

JMSType header settable through the JMS Properties table.

5.5

SMTP Sampler encodes the personal part of e-mail addresses.

5.6.3

Current release; JMS samplers compiled against javax.jms (JMS 1.1 API jar from Apache Geronimo).

6.0 (planned)

Java 17+; the master-branch JMS module uses jakarta.jms (Jakarta Messaging 3.1), so provider jars must be Jakarta clients (for ActiveMQ Classic, the 6.x line). Not yet listed in changes.xml: re-check the release notes.

References