ActiveMQ vs Valkey & Redis Based Reliable Queue and PubSub
Apache ActiveMQ is a message broker: a separate JVM process that stores messages and dispatches them to consumers. Redisson PRO's Reliable Queue and Reliable PubSub are messaging objects stored in the Valkey or Redis deployment you already run, and Redisson's JMS provider is built on them: JMS queues are backed by Reliable Queue and JMS topics by Reliable PubSub. An application written against the JMS API can move from one to the other by changing its ConnectionFactory.
That makes this a narrower comparison than most broker comparisons. If your ActiveMQ workload is JVM services exchanging JMS messages over queues and topics, and you already operate Valkey or Redis, moving removes a broker from your estate and adds JMS 2.0 features that ActiveMQ Classic still does not implement. If anything else talks to the broker, such as an MQTT device, a .NET service, a STOMP client or another broker in a network of brokers, you should stay on a broker, and the last sections of this page explain which one.
Redisson is our product. The ActiveMQ behaviour on this page comes from ActiveMQ's documentation and from tests we ran against an embedded ActiveMQ Classic 6.4.0 broker, released in October 2026, with default destination policies and client settings, using KahaDB for the redelivery, dead-letter, priority and scheduler tests; claims we tested say so. Redisson behaviour comes from the Redisson documentation and the open-source 4.8.0 API jar, which contains the Reliable Queue and Reliable PubSub interfaces. We did not test the PRO implementation.
Which ActiveMQ This Page Compares
Two brokers in use today have carried the ActiveMQ name. ActiveMQ Classic is the original 5.x and 6.x line, with the OpenWire protocol, KahaDB storage and the familiar activemq.xml. ActiveMQ Artemis grew out of the HornetQ code base as the project's next-generation broker. In November 2025 the ActiveMQ project voted to make Artemis a separate top-level project, Apache Artemis, and from version 2.50.0 its Maven artifacts moved from org.apache.activemq to org.apache.artemis.
"ActiveMQ" therefore now means Classic, and Classic is what this page compares. Artemis is a different broker with complete JMS 2.0 support, and it comes back under How to Choose.
Why ActiveMQ Classic Is Being Re-evaluated in 2026
Teams usually start looking at alternatives when an upgrade is forced on them, and 2026 forces one on many Classic installations.
- Only three lines are maintained: ActiveMQ lists 6.4, 6.3 and 5.19 as its active release lines. 6.2 became inactive after 6.2.10 in September 2026, and 5.18, 6.1 and 6.0 are end-of-life. Amazon MQ, where many Classic brokers run, offers ActiveMQ 5.19 and 5.18 as its engine versions.
- ActiveMQ has published 25 security advisories for Classic in 2026, 17 of them rated important. Five allow code execution through the web console and its Jolokia endpoint. The first, CVE-2026-34197 (CVSS 8.8), needs no credentials on 6.0.0 to 6.1.1, where it chains with CVE-2024-32114, and CISA added it to its Known Exploited Vulnerabilities catalogue on 16 April 2026. Its first fix, in 5.19.4 and 6.2.3, was bypassed twice (CVE-2026-40466 and CVE-2026-45505). End-of-life lines such as 5.18, 6.1 and 6.0 got none of these fixes. Before this, CVE-2023-46604, a CVSS 10.0 flaw in the OpenWire protocol, was used by ransomware operators against unpatched brokers in late 2023.
- Moving to 6.x is a namespace and JDK migration: ActiveMQ 6 moved to
jakarta.jmsand requires JDK 17, so a 5.x application rewrites its imports anyway. - JMS 2.0 is still incomplete. ActiveMQ documents its JMS 2.0 and Jakarta Messaging 3.1 support as partial. Version 6.4.0 added
receiveBody(), but delivery delay, shared subscriptions and asynchronous send still throwUnsupportedOperationException; we confirmed all three on 6.4.0. ActiveMQ's status page targets asynchronous send for 6.5.0 and the other two for 7.0.0.
For an installation on 5.18 or earlier, or one moving from 5.x to 6.x, a 2026 upgrade is a migration project whichever way you go, which makes it the cheapest moment to compare destinations.
ActiveMQ vs Reliable Queue and Reliable PubSub at a Glance
ActiveMQ Classic 6.4 against Redisson PRO on Valkey or Redis. "Tested" marks ActiveMQ behaviour we confirmed on an embedded 6.4.0 broker with default settings.
| ActiveMQ Classic 6.4 | Reliable Queue and Reliable PubSub | |
|---|---|---|
| Architecture | Standalone broker; clients connect over OpenWire or another protocol | Java library; messages are Valkey or Redis data, with no broker process |
| Point-to-point | JMS queues | RReliableQueue, which also backs JMS queues |
| Publish/subscribe | JMS topics and durable subscriptions; virtual topics to load-balance a subscription | RReliablePubSubTopic: named subscriptions with their own positions, each shared by any number of consumers; also backs JMS topics |
| JMS level | JMS 1.1; JMS 2.0 and Jakarta Messaging 3.1 partial | JMS 2.0, Jakarta Messaging 3.0 and 3.1; passes all TCK tests |
| JMS namespace | javax.jms on 5.x, with a client-only jakarta.jms module from 5.18.1; jakarta.jms on 6.x | Either: redisson-jms-20 for javax.jms, -30 and -31 for jakarta.jms |
| Delivery delay | JMS setDeliveryDelay() throws (tested). Broker scheduler with AMQ_SCHEDULED_* properties, off by default | Per message or per destination, in milliseconds; JMS setDeliveryDelay() is supported |
| Shared subscriptions | createSharedDurableConsumer() throws (tested) | Part of the JMS 2.0 API the provider implements; native subscriptions are shared by design |
| Message priority | Ignored by default (tested): needs prioritizedMessages on the broker or messagePrioritySupported on the client; KahaDB keeps three priority bands | 0 to 9 per message, with no configuration |
| Redelivery | Client-side policy: 6 redeliveries, 1 second apart, optional backoff (tested) | deliveryLimit, 10 attempts by default, per destination or message; on the native API a failed negative acknowledgment sets the retry delay |
| Consumer that hangs while connected | Keeps its unacknowledged and prefetched messages, up to 1,000 per queue consumer, until its connection closes (tested); an optional slow-consumer strategy can close it | Visibility timeout, 30 seconds by default, then the message goes to another consumer. A slow consumer loses it the same way |
| Reject without retrying | No API; a poison message uses up its redeliveries first | Native API: a rejected negative acknowledgment moves it to the dead-letter queue at once, or deletes it if none is configured |
| Dead letters | One shared ActiveMQ.DLQ by default; non-persistent messages are discarded instead (tested); expired persistent messages are dead-lettered | Per queue (deadLetterQueueName) or per subscription (deadLetterTopicName), none by default. Without one, a message over its limit is deleted; expired messages are removed |
| Topic retention | A message is kept until every durable subscription has acknowledged it | Kept until a time-to-live or retention mode removes it; the default keeps every message |
| Fan-out into queues | Virtual topics, composite destinations, selector-filtered destinations | Reliable Fanout into Reliable Queues, with Java filters on payload and headers |
| Message selectors | SQL-92 selectors evaluated by the broker | Through the JMS API, as the specification requires; where they are evaluated is not documented. None on the native API |
| Wildcard subscriptions | Yes, for example PRICE.STOCK.> | No |
| Replay | Limited: retroactive consumers get recent topic messages held in memory; a durable subscription can't be rewound | Reliable PubSub, native API: move a subscription to a message ID, a timestamp or the earliest retained message |
| Deduplication | No application-defined deduplication key | By ID or payload hash, over a window you choose |
| Ordering | Exclusive consumers; message groups (JMSXGroupID) | SEQUENTIAL processing mode on queues; message groups on Reliable PubSub's native API |
| Storage | KahaDB journal on disk, synced on every write by default, or JDBC | In memory, persisted by Valkey or Redis snapshots or AOF, which syncs once a second by default |
| High availability | Shared-storage or JDBC primary/replica with one active broker; a network of brokers to scale out | Valkey or Redis replication with Sentinel or Cluster; each operation can wait for replicas or the AOF |
| Flow control | Producer flow control on memory and store limits, on by default | maxSize per queue or topic; producers wait only if they pass a timeout |
| Transactions | Local transacted sessions and XA | Transacted sessions are part of the TCK, though Redisson does not document their guarantees; XA not documented for JMS destinations |
| Protocols and clients | OpenWire, AMQP 1.0, MQTT, STOMP, WebSocket; Java, C++, .NET and any client of those protocols | Java (JVM) only |
| Management | Web console, JMX, Jolokia, advisory topics | Metrics, event listeners, statistics, switching publishing or consuming off and on |
| Licence | Apache License 2.0 | Redisson PRO commercial licence, on Valkey (BSD) or Redis |
How the Messaging Models Map
Redisson's JMS provider does this mapping for you. The native API underneath is where Reliable Queue and Reliable PubSub go past JMS, and it shows how familiar ActiveMQ patterns translate.
Queues
An ActiveMQ queue becomes an RReliableQueue. Each message goes to one consumer, stays invisible to the others for its visibility timeout, and is deleted when the consumer acknowledges it. Settings that ActiveMQ spreads across activemq.xml and the client's ActiveMQConnectionFactory belong to the queue itself and can be changed at runtime:
RReliableQueue<Order> orders = redisson.getReliableQueue("orders");
orders.setConfig(QueueConfig.defaults()
// one delivery plus ActiveMQ's six redeliveries
.deliveryLimit(7)
// not acknowledged within 2 minutes: another worker gets it
.visibility(Duration.ofMinutes(2))
// no DLQ is configured unless you name one
.deadLetterQueueName("DLQ.orders"));
orders.add(QueueAddArgs.messages(MessageArgs.payload(order)
.priority(7) // 0-9, no broker policy to enable
.delay(Duration.ofMinutes(10)) // no scheduler to switch on
.deduplicationById(order.id(), Duration.ofHours(24))));
Two lines in that block need care when you translate ActiveMQ settings. Redisson documents deliveryLimit as the maximum number of delivery attempts, so 7 should match ActiveMQ's one delivery plus six redeliveries; Redisson's own default is 10, and the poison-message test in the migration steps checks the count. And there is no dead-letter queue unless you name one: without deadLetterQueueName, a message that reaches its limit is deleted.
Topics and Durable Subscriptions
An ActiveMQ topic becomes an RReliablePubSubTopic, and the subscription model changes. In JMS 1.1 a durable subscription is identified by a client ID and a subscription name, and only one consumer can be active on it at a time. That limit is why ActiveMQ users adopted virtual topics, the usual way to spread a durable subscription across several instances. JMS 2.0 fixed it with shared durable subscriptions, but ActiveMQ Classic 6.4.0 throws UnsupportedOperationException from createSharedDurableConsumer(), so on Classic virtual topics are still the answer.
In Reliable PubSub a subscription is a named, durable position in the topic, and any number of consumers can attach to it and share its messages:
RReliablePubSubTopic<OrderEvent> events =
redisson.getReliablePubSubTopic("order-events");
// Deploy step, run once: the subscription is the durable part,
// and it starts at the latest message
if (!events.hasSubscription("billing")) {
events.createSubscription(SubscriptionConfig.name("billing")
.deliveryLimit(5)
.deadLetterTopicName("order-events.billing.dlt")
// keep acknowledged messages so seek() can replay them
.retainAfterAck());
}
// In every billing instance: attach a consumer;
// the consumers share the subscription's messages
Subscription<OrderEvent> billing = events.getSubscription("billing");
PushConsumer<OrderEvent> consumer = billing.createPushConsumer();
consumer.registerListener(MessageListenerArgs.<OrderEvent>listener((msg, ack) -> {
try {
invoice(msg.getPayload());
ack.acknowledge(MessageAckArgs.ids(msg.getId()));
} catch (RuntimeException e) {
ack.negativeAcknowledge(MessageNegativeAckArgs.failed(msg.getId())
.delay(Duration.ofSeconds(30)));
}
}));
Each subscription keeps its own position, delivery limit and dead-letter topic, so a slow billing service does not delay analytics, although its backlog still occupies memory. A subscription can also be moved, which an ActiveMQ durable subscription cannot do:
// Re-run billing from the start of 3 November; other subscriptions are untouched
billing.seek(Position.timestamp(Instant.parse("2026-11-03T00:00:00Z")));
Replay depends on retention, and retention is the setting to fix first. The default topic retention mode keeps every message, including messages published when no subscription exists. A subscription removes the messages it acknowledges unless it is created with retainAfterAck(), as billing is above, so that seek() has something to replay. On an in-memory store, a busy topic with no time-to-live grows without bound. Give topics that do not need replay a timeToLive or the SUBSCRIPTION_REQUIRED_DELETE_PROCESSED retention mode, which stores a message only if a subscription exists when it is published and discards it once each of those subscriptions has acknowledged it, negatively acknowledged it or used up its delivery limit. A maxSize is not a retention control: under the default mode a full topic stops accepting messages instead of dropping old ones. JMS topics share these defaults.
Message groups exist only on Reliable PubSub's native API. Messages with the same group ID go to the same consumer, the group moves to another consumer once its owner has left the group's next message waiting and been inactive for longer than groupIdClaimTimeout, and redelivered messages in a group can arrive out of order. Reliable Queue, which backs JMS queues, has no group ID, so code that relies on JMSXGroupID on a queue needs ProcessingMode.SEQUENTIAL, which serialises the whole queue, or a move to the native API.
Virtual Topics and Composite Destinations
Virtual topics also give every consuming application a queue of its own, with its own backlog, redelivery and dead-letter handling. A producer sends to VirtualTopic.Orders and the broker copies each message into Consumer.Billing.VirtualTopic.Orders, Consumer.Fulfilment.VirtualTopic.Orders and so on. Reliable Fanout does the same with Reliable Queues, and a filter on a subscribed queue plays the role of a selector on the consumer queue:
RReliableFanout<OrderEvent> orderEvents =
redisson.getReliableFanout("orders-fanout");
// ActiveMQ: Consumer.Billing.VirtualTopic.Orders
orderEvents.subscribeQueue("billing.orders");
// ActiveMQ: a selector such as region = 'EMEA' on the consumer queue
orderEvents.subscribeQueue("emea-fulfilment.orders",
(payload, headers) -> "EMEA".equals(headers.get("region")));
orderEvents.publish(FanoutPublishArgs.messages(MessageArgs.payload(event)
.header("region", "EMEA")));
The filter is a Java predicate over the payload and headers rather than an SQL-92 string. It is applied in each publishing service as the message is published, so its class has to be available there, and changing it does not re-filter messages already queued. Publishing never waits: a subscribed queue that is full, has a message-size limit the message exceeds, or rejects it as a duplicate does not receive it, and publish() returns null when any subscribed queue missed it. Composite destinations, which forward one destination to a fixed list of others in activemq.xml, translate the same way: subscribe each target queue to one fanout.
Redelivery, Dead Letters and Poison Messages
The defaults differ most here, and a migration that copies settings line by line will still behave differently from the old system. We ran the same poison message through ActiveMQ Classic 6.4.0 on KahaDB, using a consumer in a transacted session that rolls back every delivery:
- The message was delivered seven times, about one second apart: the first delivery plus
maximumRedeliveries, which defaults to 6, with no backoff. - After the seventh delivery it was moved to
ActiveMQ.DLQ, a single queue that by default collects dead letters from every destination on the broker, with adlqDeliveryFailureCauseproperty reading "Delivery[7] exceeds redelivery policy limit". - The same message sent as
NON_PERSISTENTwas delivered seven times and then discarded. ActiveMQ does not dead-letter non-persistent messages unless the dead-letter strategy setsprocessNonPersistent="true". - Expired messages go the other way. The default strategy dead-letters expired persistent messages, so a queue with a short time-to-live and a stalled consumer fills
ActiveMQ.DLQ.
In ActiveMQ the redelivery policy lives on the client's connection factory, per destination if you use a RedeliveryPolicyMap, with an optional broker-side redelivery plugin. Dead-letter handling lives in the broker's configuration:
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(
"failover:(tcp://amq-1:61616,tcp://amq-2:61616)");
RedeliveryPolicy policy = cf.getRedeliveryPolicy();
policy.setMaximumRedeliveries(6); // the default: 7 deliveries in total
policy.setInitialRedeliveryDelay(1000); // the default
policy.setUseExponentialBackOff(true); // off by default
policy.setBackOffMultiplier(2);
<broker xmlns="http://activemq.apache.org/schema/core"
brokerName="amq-1" schedulerSupport="true">
<destinationPolicy>
<policyMap>
<policyEntries>
<policyEntry queue=">" prioritizedMessages="true">
<deadLetterStrategy>
<individualDeadLetterStrategy queuePrefix="DLQ."
useQueueForQueueMessages="true" processNonPersistent="true"/>
</deadLetterStrategy>
</policyEntry>
</policyEntries>
</policyMap>
</destinationPolicy>
</broker>
That broker block turns on four things that are off by default: the scheduler, queue priority, a dead-letter queue per destination, and dead-lettering for non-persistent messages.
ActiveMQ has no visibility timeout. A delivered message belongs to its consumer until the consumer acknowledges it or its session or connection closes, and the broker pushes up to 1,000 messages ahead to every queue consumer. We tested what that means for a consumer that hangs: it took one of ten messages and stopped, still connected, and a second consumer received nothing in the five seconds we waited. All ten went to the second consumer the moment the first connection closed. With the defaults, a thread that deadlocks inside an otherwise healthy JVM holds its whole prefetch; a smaller prefetch, or the broker's optional AbortSlowAckConsumerStrategy, which closes consumers that stop acknowledging, limits the damage.
Reliable Queue and Reliable PubSub handle these cases with three mechanisms. A polled message is invisible for the visibility timeout and then becomes available to any consumer, so a hung worker delays one message by one timeout instead of stranding a prefetch window. Every delivery attempt counts against deliveryLimit, whether it ended in a timeout or in a failed negative acknowledgment. And a consumer on the native API can say that a message will never succeed:
// long poll; manual acknowledgment is the default
Message<Order> msg = orders.poll(QueuePollArgs.defaults()
.timeout(Duration.ofSeconds(30)));
if (msg != null) {
try {
fulfil(msg.getPayload());
orders.acknowledge(QueueAckArgs.ids(msg.getId()));
} catch (InventoryTimeoutException e) {
// retry later; this spends one of the seven attempts
orders.negativeAcknowledge(QueueNegativeAckArgs.failed(msg.getId())
.delay(Duration.ofSeconds(30)));
} catch (InvalidOrderException e) {
// will never succeed: straight to DLQ.orders,
// without burning the other attempts
orders.negativeAcknowledge(QueueNegativeAckArgs.rejected(msg.getId()));
}
}
ActiveMQ has no counterpart for the rejected path: a malformed message goes to DLQ.orders immediately instead of being retried six more times. JMS has no reject call either, and Redisson's documentation does not say what delay a JMS rollback or recover() gets before redelivery. The dead-letter queue is itself a Reliable Queue, so the calls you use on a normal queue, such as listAll(), size() and move(), work on it unchanged. For the JMS view of the same mechanism, see dead letter queues and topics with Redisson's JMS API.
The visibility timeout has a cost. A consumer still working when it expires is treated like a crashed one: the message goes to another consumer and the attempt counts against the limit. ActiveMQ leaves an unacknowledged message with its consumer for as long as that consumer stays open. Set visibility above your longest processing time, JMS listeners included, and keep handlers idempotent.
Both systems can also drop a message without raising an error. ActiveMQ discards non-persistent messages that exhaust their redeliveries, which is a deliberate at-most-once choice. Redisson deletes a message that reaches its limit or is rejected when no dead-letter queue is set, removes expired messages without dead-lettering them, returns null from add() when a full queue has no room (at once without a timeout, or when the timeout expires), and leaves a message out of any subscribed queue that cannot take it when a fanout publishes. Configure dead-letter destinations everywhere, and check what add() and publish() return.
Delay, Priority and Scheduling
Three JMS features look identical in code and behave differently on the two systems. This is valid JMS 2.0; Redisson documents setDeliveryDelay() and setPriority() on its provider, which passes the JMS TCK:
try (JMSContext ctx = cf.createContext()) {
Queue reminders = ctx.createQueue("payment-reminders");
ctx.createProducer()
.setPriority(8)
// ActiveMQ Classic 6.4.0: UnsupportedOperationException
.setDeliveryDelay(Duration.ofDays(1).toMillis())
.send(reminders, "invoice-1042");
Topic events = ctx.createTopic("order-events");
// ActiveMQ Classic 6.4.0: UnsupportedOperationException
JMSConsumer billing = ctx.createSharedDurableConsumer(events, "billing");
String event = billing.receiveBody(String.class, 5000);
}
On ActiveMQ Classic 6.4.0 with default settings, we found the following:
- Delivery delay:
setDeliveryDelay()throwsUnsupportedOperationExceptionon bothJMSProducerandMessageProducer. ActiveMQ's own mechanism is the broker scheduler and theAMQ_SCHEDULED_DELAYfamily of message properties. The scheduler is off by default, and while it is off the property is ignored: a message we scheduled three seconds ahead arrived after a few milliseconds. WithschedulerSupport="true"it arrived after about 3.4 seconds. - Priority: we sent three priority-1 messages and then three priority-9 messages to an idle queue, and the consumer received them in the order they were sent. ActiveMQ uses priority only when the broker's destination policy sets
prioritizedMessages="true"or the client setsmessagePrioritySupported=true, which reorders what it has prefetched; either one delivered the priority-9 messages first in our tests. KahaDB also keeps three priority bands (below 4, 4, and above 4) rather than ten. - Shared subscriptions:
createSharedDurableConsumer()andcreateSharedConsumer()throwUnsupportedOperationException.
On Redisson, delay and priority are properties of each message, priority covers the full 0 to 9 range with no policy switch, and shared durable subscriptions sit on ordinary Reliable PubSub subscriptions. On the native API the same controls are MessageArgs.delay() and MessageArgs.priority(), shown in the queue example above. We cover priority scheduling and its starvation risks in distributed priority queues in Java.
One scheduling feature does not carry over: repetition. AMQ_SCHEDULED_PERIOD, AMQ_SCHEDULED_REPEAT and AMQ_SCHEDULED_CRON make the broker re-send a message on a schedule. Reliable Queue delays a message once, so a recurring job belongs in a scheduler that enqueues on each run, such as Redisson's scheduled executor, Quartz or Spring's @Scheduled; distributed task scheduling in Java compares them.
JMS Compatibility: What Moves Without a Rewrite
Redisson's JMS provider implements JMS 2.0, Jakarta Messaging 3.0 and Jakarta Messaging 3.1, and it passes all TCK tests, the specification's conformance suite. Code that uses only the standard API moves unchanged: connection factories, JMSContext, sessions and acknowledgment modes, producers and consumers, MessageListener, durable and shared subscriptions, and Spring's JmsTemplate and @JmsListener. The provider ships as three artifacts, so you choose the namespace independently of the migration: redisson-jms-20 for javax.jms, and redisson-jms-30 or redisson-jms-31 for jakarta.jms. A 5.x application can therefore leave ActiveMQ without first doing the javax to jakarta rewrite that ActiveMQ 6 requires.
Unchanged code still needs two Redisson defaults set during the move: every topic keeps every message unless you give it a retention mode or a time-to-live, and no destination has a dead-letter queue unless you name one. What does not move at all is everything ActiveMQ-specific. Search the code and the broker configuration for these before estimating the work:
org.apache.activemqimports:ActiveMQConnectionFactorysettings such asRedeliveryPolicy,prefetchPolicy,useAsyncSendandtrustedPackages, as well asBlobMessageand theActiveMQQueueandActiveMQTopicclasses.- Destination options embedded in names, such as
orders?consumer.prefetchSize=10orconsumer.exclusive=true. ScheduledMessageproperties (AMQ_SCHEDULED_*), andJMSXGroupIDon queues.- Virtual topic and composite destination naming, and wildcard subscriptions.
- Advisory topics (
ActiveMQ.Advisory.*) used for monitoring or presence. activemq.xmlsettings: dead-letter strategies,prioritizedMessages,schedulerSupport, memory and store limits.- JNDI properties in ActiveMQ's format.
In Spring Boot the change is a dependency and a bean. Replace spring-boot-starter-activemq with spring-boot-starter-jms on Spring Boot 4, or with spring-jms on Spring Boot 3, remove the spring.activemq.* properties, add the Redisson JMS artifact, and define the connection factory from a RedissonClient bean, for example the one Redisson's Spring Boot starter creates. Spring Boot's JMS auto-configuration builds JmsTemplate and the listener container from it, and spring.jms.* properties keep working:
@Configuration
@EnableJms
public class JmsConfig {
@Bean
public ConnectionFactory connectionFactory(RedissonClient redisson) {
RedissonConnectionFactory cf = new RedissonConnectionFactory(redisson);
cf.setClientId("orders-service"); // required for durable subscriptions
// What used to live in activemq.xml and on ActiveMQConnectionFactory
cf.setQueueConfig("orders", QueueConfig.defaults()
.deliveryLimit(7)
.visibility(Duration.ofMinutes(2))
.deadLetterQueueName("DLQ.orders"));
// A topic nobody replays: without a retention mode or a time-to-live,
// a Redisson topic keeps every message it receives
cf.setTopicConfig("price-updates", TopicConfig.defaults().retentionMode(
RetentionMode.SUBSCRIPTION_REQUIRED_DELETE_PROCESSED));
return cf;
}
}
@Component
public class OrderListener {
@JmsListener(destination = "orders")
public void onOrder(String order) {
// unchanged: the listener never knew which provider it was on
}
}
The configuration translates like this:
| ActiveMQ Classic | Redisson PRO | Note |
|---|---|---|
maximumRedeliveries (6) | deliveryLimit (10) | Documented as delivery attempts, so 6 redeliveries should become 7; confirm with a poison-message test |
initialRedeliveryDelay, useExponentialBackOff | Native API: failed negative acknowledgment with .delay(); visibility timeout for crashed consumers | The redelivery delay after a JMS rollback is not documented |
individualDeadLetterStrategy | deadLetterQueueName per queue, deadLetterTopicName per subscription | There is no shared default DLQ |
processNonPersistent | No documented equivalent | Redisson's documentation does not distinguish persistent and non-persistent delivery |
prioritizedMessages, messagePrioritySupported | Not needed | Priority 0 to 9 is always on |
schedulerSupport and AMQ_SCHEDULED_DELAY | setDeliveryDelay() or MessageArgs.delay() | No repeat or cron |
Prefetch (jms.prefetchPolicy.*) | Native API: count per poll; visibility | Redisson's documentation describes no prefetch setting |
Exclusive consumer, JMSXGroupID | ProcessingMode.SEQUENTIAL on a queue; message groups on Reliable PubSub's native API | Sequential mode serialises the whole queue, not one group |
| Virtual topics, composite destinations | Reliable Fanout, or Reliable PubSub subscriptions | One queue per consuming application, as before |
| Producer flow control, memory limits | maxSize with a timeout on add or publish | Without a timeout a full queue turns the message away at once; see backpressure with Reliable Queue |
| Topic messages dropped once every durable subscription has them | retentionMode SUBSCRIPTION_REQUIRED_DELETE_PROCESSED, or a timeToLive, per topic | The default keeps every message |
ActiveMQInitialContextFactory, queue.orders = ORDERS | RedissonInitialContextFactory, queue.orders.name = ORDERS | The two-part form creates no binding; dynamicQueues/ and dynamicTopics/ lookups work as before |
The JNDI row is the easiest to miss: ActiveMQ-style queue.orders = ORDERS lines create no binding in Redisson's context, so the lookup fails. JMS messaging over Valkey and Redis walks through the JNDI and Spring set-up in full, and the Jakarta Messaging 3.1 walkthrough covers queues, topics and durable subscriptions step by step.
Durability and High Availability
ActiveMQ writes persistent messages to disk: to KahaDB, the default store, which by default syncs each write before acknowledging it, or to a JDBC database. High availability is primary/replica over shared storage: the brokers share a file system or a database, one holds the lock and serves clients, and the others wait to take over, while clients find the active broker through the failover: transport. To scale out, brokers form a network and forward messages between them. Because the backlog sits on disk, a queue can hold far more messages than the broker has RAM. The replicated LevelDB store, the shared-nothing option, was removed in 5.17.
Reliable Queue and Reliable PubSub store messages in Valkey or Redis, so durability is whatever that deployment provides, plus one control of their own:
- Persistence is a server setting. Valkey and Redis hold data in memory and persist it with RDB snapshots or the append-only file (AOF); snapshots alone can lose every write since the last snapshot, so enable AOF for messaging. With AOF on, Valkey and Redis sync it to disk once a second by default (
appendfsync everysec), so a crash can lose up to a second of writes unless the operation waits for the AOF. Our guide to Redis persistence covers the options. - Each operation can wait for durability. Replication is asynchronous, so Reliable Queue and Reliable PubSub let every operation choose a
syncMode.ACKwaits until replicas have applied the write, andACK_AOFwaits until it is in the AOF on the primary and the replicas, which needs Redis 7.2 or later or any Valkey version. The default,AUTO, uses AOF confirmation when AOF is enabled, replica acknowledgment when it is not, and no guarantee when neither is available. Waiting adds that time to every operation, and it does not make failover lossless: a write that has not reached the replica that gets promoted can still be lost. - The failure default is lenient. If the sync target is not met within
syncTimeout(one second by default), the defaultsyncFailureModeisLOG_WARNING: the operation logs a warning and returns as if it had succeeded. For messages you cannot lose, setTHROW_EXCEPTIONand handle it. - The backlog lives in RAM. A consumer outage that would have grown a KahaDB journal grows Valkey or Redis memory instead. Bound queues with
maxSizeand giveadd()a timeout so that producers wait, and size the instance for the largest backlog you are prepared to hold. - Eviction must not touch messaging data. An instance configured as a cache with an
allkeys-*eviction policy can evict queue keys under memory pressure. Run messaging on an instance withnoeviction, or at least not on the cache.
High availability comes from Valkey or Redis: replicas with Sentinel for automatic failover, or Cluster for sharding. There is no broker to fail over, and Redisson follows the new primary itself. Managed services run that part for you, though they differ in the persistence options they expose, so check that AOF is available before you rely on ACK_AOF.
Operations and Security
Leaving ActiveMQ removes a component: broker JVMs and their storage, the OpenWire listener, and the web console with its Jolokia endpoint. ActiveMQ's worst vulnerabilities have been in the last two: CVE-2023-46604 in OpenWire, and the 2026 run of code-execution flaws through Jolokia and the web console.
You still have to patch, because your messaging now depends on Valkey or Redis, which has had serious vulnerabilities of its own: CVE-2025-49844, a use-after-free in the Redis Lua engine rated 10.0, was fixed in October 2025. Redisson implements its atomic operations with Lua scripts, so disabling scripting is not an available mitigation; patching is. The gain is one data platform to secure, monitor and patch instead of two, probably one you already treat as critical. The cost is capacity: messaging needs a Valkey or Redis deployment configured and sized for it, which may mean a dedicated one.
Monitoring moves as well. ActiveMQ reports queue depth, consumer counts and enqueue and dequeue counts through JMX and the web console, and publishes broker events on advisory topics. Reliable Queue publishes metrics under redisson.reliable-queue.<name>, including additions, polls, acknowledgments, failed and rejected acknowledgments and failed synchronisations. Reliable PubSub reports statistics per topic, subscription and consumer. Both offer event listeners and counts of ready, delayed and unacknowledged messages, and both can switch publishing or consuming off for maintenance and back on again. Monitoring Redis from Java shows how to export the client metrics.
How to Choose
Stay on ActiveMQ Classic if non-Java clients or other protocols use the broker, if you run a network of brokers, if backlogs regularly outgrow memory, or if you depend on XA with JMS. Run the latest patch release of an active line, which was 5.19.12, 6.3.3 or 6.4.0 when this was written, and keep the web console off untrusted networks. If running the broker is the problem rather than ActiveMQ itself, Amazon MQ will operate 5.19 for you.
Move to Apache Artemis if you want to keep a broker and get the JMS 2.0 features Classic lacks. Artemis has full JMS 2.0 and Jakarta Messaging 3.1 support, speaks AMQP, MQTT, STOMP and OpenWire, so existing Classic clients can connect, and publishes migration documentation that covers virtual topics.
Move to Reliable Queue and Reliable PubSub if every producer and consumer runs on the JVM, you already run Valkey or Redis in production, and your usage is standard JMS queues and topics. You stop operating a broker and gain delivery delay, shared subscriptions and priority without configuration, with visibility timeouts in place of prefetch. On the native API you also get rejection straight to the dead-letter queue, deduplication and replay.
If you are also weighing Kafka or RabbitMQ, those are different trades. Kafka replaces a queue with a log, which changes delivery semantics; see Redis vs Kafka and Apache Kafka alternatives. RabbitMQ reaches JMS through a client library with documented gaps in its open-source edition, and through a JMS queue type in the commercial Tanzu edition from 4.3; see RabbitMQ alternatives. IBM MQ estates have their own comparison in IBM MQ vs Kafka.
Moving From ActiveMQ to Redisson, Step by Step
- Inventory. Search the code and
activemq.xmlfor the ActiveMQ-specific items listed under JMS compatibility, fromorg.apache.activemqimports to dead-letter strategies. Each hit maps to a row in the mapping table, or to a reason to stay. - Choose the artifact by namespace. Use
redisson-jms-20if the code still importsjavax.jms, andredisson-jms-31forjakarta.jms. - Prepare Valkey or Redis for messaging. Turn on AOF, add replicas, set
noeviction, provision memory for your largest backlog, and decide onsyncModeandsyncFailureMode. If the existing instance is a cache, a dedicated one is simpler. - Swap the connection factory and translate the configuration using the mapping table. Set a dead-letter queue on every queue and a
deadLetterTopicNameon every subscription, because in ActiveMQ every destination dead-lettered toActiveMQ.DLQby default and in Redisson none does, and give every topic a retention mode or a time-to-live. - Cut consumers over first. Start consumers on Redisson alongside the old ones, then move producers, and leave the ActiveMQ consumers running until the old queues drain. No message sits in both systems. The same order works for topics, provided the Redisson subscriptions exist before the first producer switches: a new subscription starts at the latest message by default, and a topic in a
SUBSCRIPTION_REQUIRED_*retention mode does not store messages published while it has no subscription. - Test the failure paths. Before going live, send a poison message and check that it is delivered the number of times you expect and then reaches the dead-letter queue, kill a consumer mid-message and check that it is redelivered after the visibility timeout, and fail over the Valkey or Redis primary under load with your chosen
syncMode.
One trap catches people who prototype against the open-source Redisson jar. The Reliable Queue and Reliable PubSub interfaces ship in it, so the code compiles, and getReliableQueue() then throws UnsupportedOperationException at runtime. Reliable Queue, Reliable PubSub, Reliable Fanout and the JMS provider all need Redisson PRO.
Frequently Asked Questions
Can Redis Replace ActiveMQ?
For JVM applications that use standard JMS queues and topics, yes. Redisson PRO implements JMS 2.0 and Jakarta Messaging 3.1 on Valkey or Redis, backed by Reliable Queue and Reliable PubSub, which add acknowledgments, delivery limits, dead-letter queues, delivery delay and priority. Plain Redis Pub/Sub is not a replacement: it delivers only to subscribers connected at that moment and stores nothing. Stay on a broker if non-Java clients, MQTT or AMQP, networks of brokers or XA with JMS are involved.
Does ActiveMQ Classic Support JMS 2.0?
Partially. ActiveMQ Classic 5.18 and later implement the JMS 2.0 simplified API (JMSContext, JMSProducer and JMSConsumer), and 6.4.0 added receiveBody(). Delivery delay, shared subscriptions and asynchronous send with a CompletionListener still throw UnsupportedOperationException in 6.4.0, and ActiveMQ targets them for 6.5.0 and 7.0.0. Apache Artemis supports JMS 2.0 in full.
What Is the Difference Between ActiveMQ Classic and Apache Artemis?
They are different brokers. Classic is the original ActiveMQ code base, with OpenWire and KahaDB. Artemis grew out of HornetQ and has full JMS 2.0 and Jakarta Messaging 3.1 support with its own storage and clustering design. In November 2025 it left the ActiveMQ project to become a separate Apache top-level project, Apache Artemis, and its Maven artifacts moved to the org.apache.artemis group from version 2.50.0.
Where Do Failed Messages Go in ActiveMQ?
With the default redelivery policy, a persistent message that fails its first delivery and six redeliveries moves to ActiveMQ.DLQ, one dead-letter queue shared by every destination unless you configure individualDeadLetterStrategy. Non-persistent messages are discarded instead, unless the strategy sets processNonPersistent to true. Expired persistent messages are dead-lettered by default as well.
Is Reliable PubSub the Same as Redis Pub/Sub?
No. Valkey and Redis Pub/Sub deliver a message to whoever is subscribed at that moment and store nothing, so a subscriber that is offline misses it. Reliable PubSub stores messages, tracks each subscription's position, supports manual acknowledgment, redelivers after a visibility timeout and, when a dead-letter topic is configured, moves failed messages to it. It is a Redisson PRO feature built on Valkey or Redis.
Do I Have to Rewrite My JMS Code to Move Off ActiveMQ?
Not if it uses only the JMS API. Replace the ConnectionFactory with Redisson's, keep javax.jms with redisson-jms-20 or move to jakarta.jms with redisson-jms-31, and Spring's JmsTemplate and @JmsListener keep working. Plan to set dead-letter queues and topic retention, which Redisson leaves off by default, and to change ActiveMQ-specific code such as ActiveMQConnectionFactory settings, scheduled-message properties, virtual topic names, advisory topics and JNDI properties in ActiveMQ's format.
Do Reliable Queue and Reliable PubSub Work With Valkey?
Yes. Reliable Queue, Reliable PubSub and the JMS provider run on Valkey or Redis, self-hosted or managed. The ACK_AOF synchronisation mode needs Redis 7.2 or later, and any Valkey version supports it.
Next Steps
All about Reliable Queue and all about Reliable PubSub cover the two objects in detail, and what a JMS broker does compares JMS providers by the specification level each one implements. Full configuration is in the messaging documentation.
Reliable Queue, Reliable PubSub and the JMS API are part of Redisson PRO. You can try it for free.
Similar articles
- Apache Kafka vs Valkey & Redis Based Reliable PubSub
- Redis vs. RabbitMQ: Choosing the Right Message Broker
- Apache Pulsar vs. Valkey & Redis based Reliable PubSub
- Google Pub/Sub vs Redis for Messaging
- Amazon SQS vs. Reliable Queue for Valkey and Redis
- Azure Service Bus vs Valkey & Redis Based Reliable Queue and PubSub
- IBM MQ vs Kafka: When Kafka Replaces a Queue, and When It Cannot
- TIBCO EMS vs Kafka: Key Differences and What Breaks When You Migrate