TIBCO EMS vs Kafka: Key Differences and What Breaks When You Migrate

Published on
November 5, 2026

If you are comparing TIBCO EMS with Kafka, you probably run EMS already, with years of integrations and a mix of Java, .NET and C clients behind it, and someone has asked whether Kafka can take over. The two overlap less than the question suggests. EMS is a message broker that implements the JMS specification: it hands each queued message to one consumer, waits for the acknowledgement and then deletes the message. Kafka is a partitioned log that keeps records after they are read, so any number of consumers can read them again.

That broker-versus-log difference is the same one we work through in IBM MQ vs Kafka, so this page does not repeat it. Two things make EMS a separate question. EMS extends JMS with its own constants and properties, such as RELIABLE_DELIVERY, NO_ACKNOWLEDGE and the $sys.undelivered queue, and anything that uses them runs only on EMS, so those extensions decide how much of the codebase a move touches. And since EMS 10.5.0, released in February 2026, TIBCO ships its own transport between EMS and Apache Kafka, so the realistic plan is often EMS and Kafka together for a while.

Redisson is our product. We sell neither EMS nor Kafka; Redisson PRO implements JMS on Valkey and Redis, and it is one of the options covered near the end of this page, limits included. EMS details were checked against the 10.5.1 documentation (September 2026) and Kafka details against Apache Kafka 4.4.0 (October 22, 2026).

TIBCO EMS vs Kafka at a Glance

TIBCO EMSApache Kafka
What it isA JMS broker: a server that stores messages until they are consumedA distributed, partitioned, replicated log
Standard APIJakarta Messaging 2.0 (the javax.jms version) and 3.0; TIBCO reports passing both TCKsKafka's own client API; not a JMS provider
Client languagesJava, .NET, C and COBOLJava in the Apache project; other languages through separate clients such as librdkafka
Consumption modelQueues (each message goes to one consumer) and topics with durable subscriptionsConsumer groups read partitions by offset; share groups add queue-style consumption
After consumptionDeleted once acknowledgedKept until time or size retention, or compaction, removes it
ReplayNoYes, from an offset or a timestamp
AcknowledgementJMS modes plus EMS-only NO_ACKNOWLEDGE, EXPLICIT_CLIENT_ACKNOWLEDGE and EXPLICIT_CLIENT_DUPS_OK_ACKNOWLEDGEOffset commits per partition; per-record accept, release, reject or renew in share groups
Delivery modesPERSISTENT, NON_PERSISTENT, RELIABLE_DELIVERYEvery record is written to the log; durability comes from acks, replication and min.insync.replicas
Server-side filteringJMS message selectorsNone
Redelivery limit and dead lettersmaxRedelivery per queue; $sys.undelivered only for messages the producer flaggedConsumer groups: nothing on the broker. Share groups: a delivery-attempt limit (5 by default) and, since 4.4, an optional dead-letter topic
Message expiryPer message (JMS time-to-live)None per record; retention deletes old log segments, read or not
Priority and delayed deliveryBothNeither
TransactionsLocal JMS transactions and XATransactions within Kafka only; no XA
OrderingPer producer and destination; exclusive queues keep a single active consumerPer partition
High availabilityA fault-tolerant server pair on shared storage, or FTL stores replicated across three serversPartition replication across brokers, with KRaft controllers
Between sitesRoutes between servers for global destinations; disaster recovery built in with FTL storesMirrorMaker 2 or a vendor's replication
LicenceCommercial; a Community Edition covers development, test and limited production useApache License 2.0
Current release10.5.1 (September 2026)4.4.0 (October 2026)

What TIBCO EMS Gives You

TIBCO Enterprise Message Service is a JMS broker from TIBCO, which has been part of Cloud Software Group since Citrix was taken private and combined with TIBCO in 2022. According to its user guide, EMS 10.5 passed the Eclipse Foundation TCKs for Jakarta Messaging 2.0, the javax.jms version that corresponds to JMS 2.0, and for Jakarta Messaging 3.0, provided the server parameter jms_2_0_compliance is set to true. Since release 10.2, EMS has shipped two sets of client jars, one for the javax.jms namespace and one for jakarta.jms, so applications on either side of the Jakarta rename can use the same server. TIBCO makes no Jakarta Messaging 3.1 claim.

Its reach beyond Java is often why it is still in place. The same server accepts Java, .NET, C and COBOL clients, and release 10.5.1 supports .NET 10 and .NET Framework 4.8.1. EMS also takes part in XA transactions: the Java client provides TibjmsXAConnectionFactory, whose documentation notes that XA sessions do not support routed queues, and the 10.5.1 release notes say distributed transactions work with .NET Framework but not with .NET.

A good deal of routing happens inside the EMS server, and that is the part a migration has to account for:

  • Message selectors filter on headers and properties before delivery.
  • Exclusive queues send every message to one consumer while others wait on standby.
  • Destination bridges copy messages from a topic to one or more queues, or from a queue to a topic, with wildcards.
  • Routes forward messages between EMS servers for destinations marked global.
  • Per-destination limits (maxmsgs, maxbytes, overflowPolicy, flowControl) reject new messages, discard old ones or slow producers down.
  • Redelivery settings (maxRedelivery, redeliveryDelay) and the undelivered message queue decide what happens to messages that keep failing.

For availability there are two designs. The long-standing one is a fault-tolerant pair: two servers, one active, sharing a file-based store on storage that meets TIBCO's shared-state criteria. The 10.x line adds FTL stores, which need a cluster of three FTL servers, replicate the store to a majority of them and run one of the three EMS servers as standby_only. TIBCO suggests FTL stores where shared storage is unavailable or too slow, and gives cloud deployments as the example.

EMS is still developed: 10.4.0 shipped in February 2025, 10.5.0 in February 2026 and 10.5.1 in September 2026. Releases also leave support. TIBCO ended support for 10.3.0 on March 31, 2026, so staying on EMS means upgrading on TIBCO's schedule.

EMS keeps no history. An acknowledged message is deleted, so there is no offset to rewind and no way to run last week's traffic through new code. If replay is what drives the Kafka proposal, the requirement is real, and no EMS setting meets it. Kafka does, and so does Reliable PubSub in Redisson PRO, for JVM clients and a history that fits in memory.

What Kafka Gives You, and What It Asks For

Kafka stores records in partitioned, replicated, append-only logs. Reading removes nothing: records stay until a retention limit or compaction removes them, and each consumer group tracks its own offset in each partition. That design gives Kafka replay, independent readers moving at different speeds, high throughput from sequential writes, and a large ecosystem for stream processing and connectors. Ordering holds only within a partition. The longer version of the queue-versus-log argument is in IBM MQ vs Kafka and Is Kafka a Message Queue?

Three recent releases matter for an EMS comparison. Kafka 4.0 (March 2025) removed ZooKeeper, so a cluster is now brokers plus KRaft controllers. Kafka 4.2 (February 2026) declared share groups production-ready: consumers in a share group compete for records regardless of partition count, the broker holds an acquisition lock on each delivered record (30 seconds by default) and counts delivery attempts against a limit (five by default), and the consumer accepts, releases, rejects or renews each record. Kafka 4.4 (October 22, 2026) added dead-letter queues for share groups under KIP-1191, available once the cluster's share.version feature is upgraded to 2 and a group names a dead-letter topic. That is the closest Kafka has come to EMS queue semantics. Share groups still give up ordering to scale consumers, and Kafka still has no message priority, no delayed delivery and no server-side filtering.

Kafka does not implement JMS. Code written against javax.jms or jakarta.jms is rewritten against Kafka's client API. The best-known JMS layer for Kafka, Confluent's JMS client, implemented JMS 1.1 and was removed in Confluent Platform 8.0 in June 2025; the 7.9 line, the last one that ships it, reaches the end of standard support in February 2027. It never supported message selectors, durable subscribers, transactions or temporary destinations, and it ignored JMSPriority and JMSExpiration.

The EMS Features With No Kafka Equivalent

Each row below is something EMS applications rely on in production, and on the Kafka side most of them turn into application code.

EMS featureWhat it does on EMSKafkaWork to replace it
RELIABLE_DELIVERYProducer sends without waiting for a server receipt, whatever the authorization setting; the message is never storedacks=0 stops the wait, but the record is still written to the logChoose acks per producer
NON_PERSISTENTThe message is never written to the storeNo equivalent: every record is written to the logSet replication and min.insync.replicas per topic, acks per producer
NO_ACKNOWLEDGEConsumer never acknowledges and the server never redeliversNo direct equivalent; committing offsets before processing gives at-most-onceRewrite the consumer loop
EXPLICIT_CLIENT_ACKNOWLEDGEAcknowledges one message at a time, in any orderConsumer groups: no, commits are cumulative per partition. Share groups: per-record acceptMove to share groups, or rework the processing order
maxRedelivery, redeliveryDelayLimits deliveries per queue (2 to 255) and holds returned messages for 15 seconds to 8 hoursShare groups: a delivery-attempt limit (5 by default), no delay. Consumer groups: nothing on the brokerRetry topics or a delay mechanism in the client
$sys.undeliveredKeeps expired or over-delivered messages, if the producer flagged themShare groups: an optional dead-letter topic since 4.4, holding headers only unless configured to copy records. Consumer groups: client-side onlyConfigure per share group or per framework
Message expirationAn unconsumed message expires after its time-to-live and is discarded, or kept in $sys.undelivered if flaggedNo per-record expiry; retention deletes old log segments, read or notAn expiry header that consumers check
Message selectorsThe server filters on headers and propertiesNoneFilter in the consumer, or split into more topics
Exclusive queuesOne active consumer, the others on standbyOne consumer per partition in a groupPartition design
Priority and delivery delayHigher priority first when messages are waiting; setDeliveryDelay()NoneTopics per priority; a scheduler or delay topics
Destination bridgesThe server copies topic messages to queues, or the reverseEvery consumer group reads the whole topic anywayUsually less: most bridges disappear
Routes and global destinationsForward messages between EMS serversMirrorMaker 2 between clustersA separate replication pipeline
maxmsgs, overflowPolicy, flowControlLimit what may wait; reject, discard the oldest or slow producersRetention limits what is kept, consumed or not; quotas throttle clientsSize retention to the slowest consumer
XA transactionsEMS and a database commit or roll back togetherTransactions cover Kafka reads and writes onlyOutbox pattern and idempotent consumers
Temporary queuesReply destinations for request/replyNo temporary topicsReply topics and correlation IDs

Delivery and Acknowledge Modes

EMS adds a third delivery mode to the two that JMS defines. NON_PERSISTENT and RELIABLE_DELIVERY messages are never written to persistent storage. A PERSISTENT message sent to a queue is written to the store; one published to a topic is written only if the topic has a durable subscriber or a subscriber with a fault-tolerant connection, and writes are asynchronous unless the store is set to mode = sync. The two non-stored modes differ in what the producer waits for. A NON_PERSISTENT send waits for the server's confirmation by default only when authorization is enabled, and the npsend_check_mode parameter changes that. A RELIABLE_DELIVERY send never waits: in TIBCO's words, "the server never sends the producer a receipt confirmation or access denial and the producer does not wait for it". It succeeds unless the connection to the server has failed.

Kafka has no counterpart to a message that skips storage. Every record is appended to a partition log and replicated according to the topic's replication factor. A producer chooses only how long it waits, through acks (0, 1 or all). acks=0 is the nearest thing to RELIABLE_DELIVERY, because the producer does not wait for the broker, but the record still lands in the log and costs disk and replication like any other.

On the consumer side EMS adds three acknowledge modes. With NO_ACKNOWLEDGE consumers never acknowledge and "the server never redelivers messages". The standard CLIENT_ACKNOWLEDGE mode acknowledges everything the session has received so far; EXPLICIT_CLIENT_ACKNOWLEDGE changes that so each call acknowledges one message, in any order, and EXPLICIT_CLIENT_DUPS_OK_ACKNOWLEDGE does the same lazily.

Kafka's classic consumer groups cannot acknowledge one message at a time. A committed offset means everything before that position in the partition is done, so one slow message holds back the commit for every message after it. Share groups changed that: each record is accepted, released for another attempt, rejected, or renewed while work continues. If your EMS consumers use EXPLICIT_CLIENT_ACKNOWLEDGE to finish messages out of order, share groups are the model to migrate to, and they are production-ready from Kafka 4.2. Two constraints come with them: in explicit mode every record from one poll must be acknowledged before the next poll, and the consumer is not thread-safe, so handing records to worker threads that acknowledge on their own needs a different design.

Whatever the destination, inventory these extensions first, because code that uses them runs only on EMS, whether you are moving to Kafka or to another JMS provider. In Java they are the constants on com.tibco.tibjms.Tibjms and the JMS_TIBCO_ properties; in .NET code look for the TIBCO.EMS namespace, and in C for TIBEMS_ constants. The modes are plain integers (22, 23 and 24), so search configuration as well, for example a Spring sessionAcknowledgeMode. On the server side, queues.conf, topics.conf, bridges.conf and routes.conf hold the destination properties, bridges and routes.

Redelivery and the Undelivered Message Queue

EMS limits redelivery per queue. maxRedelivery accepts a value from 2 to 255, with 0 meaning no limit; consumers can read the current count from JMSXDeliveryCount, and redeliveryDelay can hold a returned message back for anything from 15 seconds to 8 hours. The count is not kept across a server restart.

EMS has a dead-letter destination, the system queue $sys.undelivered, but a message that exceeds the limit goes there only if its producer set the boolean property JMS_TIBCO_PRESERVE_UNDELIVERED to true before sending. Otherwise, as the EMS documentation puts it, "the message is deleted by the server". The flag cannot be set on a queue or topic, only on each message, and $sys.undelivered is never routed to other servers. The same rule covers messages that expire, and delayed messages that come due when their destination has reached its maxmsgs limit and its overflowPolicy is rejectIncoming.

On Kafka the answer depends on the consumer model. Classic consumer groups have no dead-letter mechanism on the broker, because the broker keeps no per-message delivery state; frameworks add one in the client, such as Spring Kafka's dead-letter publishing, Kafka Connect's errors.deadletterqueue.topic.name and, since 4.2, Kafka Streams' exception handlers. Share groups count deliveries against a limit, five by default. Since Kafka 4.4, once share.version is upgraded to 2, a share group can name a dead-letter topic for records that exceed the limit or that a consumer rejects. By default the dead-letter record carries only headers naming the original topic, partition, offset and delivery count; it copies the payload only if errors.deadletterqueue.copy.record.enable is true. The target topic has to accept dead letters through errors.deadletterqueue.group.enable, and its name must start with dlq. unless the prefix is changed. If the write fails with an error a retry cannot fix, the broker logs it and archives the record without a copy. Before 4.4, records past the limit were only archived: no longer delivered, but kept in the log until retention removed them.

On Kafka the choice belongs to the group or the framework, not to each message, so a group with a dead-letter topic also keeps the messages that EMS producers deliberately left unflagged. The general pattern, and what to do with messages once they arrive, is covered in dead letter queue.

Selectors, Exclusive Queues and Bridges

Selectors are the largest of the three. An EMS consumer can ask for region = 'EMEA' and receive nothing else, because the filter runs before delivery. Kafka has no server-side filter: consumers read everything and discard what they do not want, or producers split the stream into more topics so that the filter becomes a choice of topic. Inside a share group there is an extra catch, shown in the code below. A record the consumer does not want still has to be acknowledged before the next poll. Accepting it removes it for the whole group, and releasing it only sends it round the group again until the delivery limit, so each filter needs its own share group, and each of those groups reads the full topic.

Exclusive queues give one consumer every message while the others wait on standby, and the server promotes a standby consumer if the active one fails. Kafka gets the same effect from partition assignment: within a group each partition has one consumer at a time, so a single-partition topic behaves like an exclusive queue, throughput limit included.

Bridges are the one case that tends to get simpler. An EMS bridge copies messages from a topic to one or more queues, or the other way, so that several applications each get their own queue fed from one topic. On Kafka every consumer group already reads the whole topic independently, so most bridges disappear. Routes between EMS servers, which forward messages for destinations marked global, become cluster-to-cluster replication with MirrorMaker 2.

Transactions and XA

An EMS application can consume a message and update a database in one XA transaction, so both commit or both roll back. Kafka's transactions cover reading from and writing to Kafka, which is what exactly-once stream processing needs, and they cannot include a database. On Kafka the same application becomes a local database transaction plus the outbox pattern, with idempotent consumers downstream. IBM MQ vs Kafka covers that trade in more detail; nothing about it is specific to EMS.

The Same Consumer on EMS and on Kafka

The EMS code below has a producer that asks EMS to keep its message if it cannot be delivered, and a consumer that takes only EMEA orders from a shared queue and acknowledges each one when it has been processed.

import com.tibco.tibjms.Tibjms;
import com.tibco.tibjms.TibjmsConnectionFactory;
import javax.jms.Connection;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;

TibjmsConnectionFactory factory = new TibjmsConnectionFactory("tcp://ems:7222");
Connection connection = factory.createConnection("orders-app", password);
Session session = connection.createSession(false,
        Tibjms.EXPLICIT_CLIENT_ACKNOWLEDGE);
Queue orders = session.createQueue("orders");

// Producer: without this property EMS deletes the message when it expires
// or exceeds the queue's maxRedelivery, instead of moving it to
// $sys.undelivered.
MessageProducer producer = session.createProducer(orders);
TextMessage order = session.createTextMessage(json);
order.setStringProperty("region", "EMEA");
order.setBooleanProperty("JMS_TIBCO_PRESERVE_UNDELIVERED", true);
producer.send(order);

// Consumer: the server delivers only EMEA orders, and acknowledge()
// confirms this message alone, so other messages can still be in flight.
MessageConsumer consumer = session.createConsumer(orders, "region = 'EMEA'");
connection.start();
Message received = consumer.receive();
process(received);
received.acknowledge();

The closest Kafka version uses a share group with explicit acknowledgement, production-ready from Kafka 4.2. The filter moves into the consumer, and the dead-letter decision moves from the message to the group.

import static java.nio.charset.StandardCharsets.UTF_8;

import java.time.Duration;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.consumer.AcknowledgeType;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaShareConsumer;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.serialization.StringDeserializer;

Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("group.id", "orders-emea");     // one share group per filter
props.put("share.acknowledgement.mode", "explicit");
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", StringDeserializer.class.getName());

try (KafkaShareConsumer<String, String> consumer =
         new KafkaShareConsumer<>(props)) {
    consumer.subscribe(List.of("orders"));
    while (running) {
        ConsumerRecords<String, String> batch =
                consumer.poll(Duration.ofSeconds(1));
        for (ConsumerRecord<String, String> record : batch) {
            Header region = record.headers().lastHeader("region");
            boolean emea = region != null && region.value() != null
                    && "EMEA".equals(new String(region.value(), UTF_8));
            // No selector: every record reaches this group, and explicit mode
            // needs an acknowledgement for each one before the next poll.
            // Releasing other regions would only cycle them through the group.
            if (!emea) {
                consumer.acknowledge(record, AcknowledgeType.ACCEPT);
                continue;
            }
            try {
                process(record);
                consumer.acknowledge(record, AcknowledgeType.ACCEPT);
            } catch (RuntimeException e) {
                // Back to the group for another attempt. Past the delivery
                // limit (5 by default) the record is archived, or, on Kafka
                // 4.4, written to the group's dead-letter topic if it has one.
                consumer.acknowledge(record, AcknowledgeType.RELEASE);
            }
        }
        // Failed acknowledgements are returned, not thrown. A record whose
        // lock expired before it was acknowledged will be delivered again.
        consumer.commitSync().forEach((partition, error) ->
                error.ifPresent(e -> onAckFailure(partition, e)));
    }
}

Four things change in every consumer ported this way. The filter now costs a full read of the topic per share group, where EMS evaluated it on the server and sent each consumer only the matching messages. The producer's choice to preserve a message is gone, replaced by the group's dead-letter setting. Processing time is bounded: a record that is not acknowledged within its acquisition lock, 30 seconds by default and counted from the fetch, goes to another consumer, so slow handlers need AcknowledgeType.RENEW, or share.acquire.mode=record_limit with a small max.poll.records so that records are not fetched before they can be worked on. And nothing here is JMS, so producers, consumers, tests and monitoring are rewritten rather than reconfigured.

Running EMS and Kafka Side by Side

EMS 10.5.0 added a transport for Apache Kafka. An EMS topic configured to export sends its messages to the Kafka topic of the same name, and one configured to import receives messages from it. TIBCO lists Apache Kafka 4.0.0 or later as compatible and supports the transport on Linux only. Exported messages change shape on the way: the body always becomes a JSON string, so a TextMessage arrives as {"_text": ...} and a BytesMessage as base64 under _bytes, and JMS headers and properties become Kafka headers with string values. Kafka consumers have to unwrap the payload, and since TIBCO documents key mapping only for imports (10.5.1 copies an incoming Kafka key into the JMS_TIBCO_IMPORTED_KEY property), check how exported records are keyed before relying on per-key ordering. The details are in TIBCO's Kafka interoperation guide.

Three more limits shape a migration plan. The documentation describes the transport for EMS topics only, so queues need another route. Wildcard topics cannot import. And the server re-imports Kafka messages from the last committed offset, which TIBCO notes "may lead to message duplication", so consumers on the EMS side must tolerate duplicates. TIBCO also warns against configuring round trips between EMS and Kafka.

Before 10.5 there was no direct link. TIBCO's documented route went from EMS to TIBCO FTL and on through the separate TIBCO Messaging Bridge for Apache Kafka; the latest release we found is 2.4.0, from January 2023, and the 2.3.x releases left support on December 31, 2024. Outside TIBCO, Confluent sells TIBCO Source and Sink connectors for Kafka Connect. The source reads an EMS queue, its default, or a topic, optionally through a selector, which makes it one way to bring queues across. It handles TextMessage and BytesMessage but not ObjectMessage or StreamMessage, delivers at least once, and needs a Confluent subscription after a 30-day trial.

Whichever bridge you use, the low-risk order is the usual strangler sequence. Export a topic, add Kafka consumers while the EMS consumers keep running, and retire the EMS side once nothing reads from it. Make every consumer idempotent along the way: the Confluent connector is at-least-once, TIBCO warns of duplicates on re-import, and its documentation states no delivery guarantee for export. Leave until last the queues that depend on the extensions above, especially selectors, XA and per-message dead-lettering, or move those to a JMS provider instead.

Cost, Support and Operations

EMS is commercial software. TIBCO sells an Enterprise Edition and offers a Community Edition for "free development, test, and limited production use". Licence cost is one reason these evaluations start. Procurement firms publish figures for TIBCO renewal increases, but without a method, so we do not repeat them; your own renewal quote is the number to use.

The support calendar is a cost too. TIBCO announced in March 2025 that support for EMS 10.3.0 would end on March 31, 2026, and said that until then the release would get answers to questions but no product updates, security updates included. An EMS estate therefore budgets for regular upgrades, as it would for any other server.

Kafka's licence costs nothing and running it does. A production cluster means brokers and KRaft controllers, disk sized to the retention period rather than to the backlog, monitoring for consumer lag, and people who know what to do when replicas fall out of sync. Managed services turn that work into a bill. Retention is easy to underestimate coming from EMS: EMS stores what has not been consumed yet, while Kafka stores everything for as long as the retention policy says, whether it has been consumed or not.

We do not publish prices, because TIBCO quotes and managed Kafka price lists change too often for a table to stay right. In most of these projects the deciding cost is the migration itself, and it grows with every row of the extensions table above that your applications use.

If Your EMS Workload Is Mostly JMS Queues and Topics

Some EMS estates use little beyond standard JMS: queues, durable topic subscriptions, a redelivery limit and somewhere for failed messages to go. For those workloads, moving to Kafka is a rewrite that buys replay you may not need. The alternative is to keep the JMS code and change the provider.

Redisson PRO's JMS implementation is one such provider. It implements JMS 2.0 (JSR 343), Jakarta Messaging 3.0 and 3.1, and its documentation states that it "successfully passes all TCK tests". Queues are backed by Reliable Queue (RReliableQueue) and topics by Reliable PubSub (RReliablePubSubTopic), stored in Valkey or Redis. There is no separate broker process, although the Valkey or Redis deployment then needs messaging settings rather than cache settings: persistence, replicas and an eviction policy that never removes messages. Applications on javax.jms use the pro.redisson:redisson-jms-20 artifact, and those on jakarta.jms use redisson-jms-30 or redisson-jms-31. JNDI lookups work through RedissonInitialContextFactory, and Spring Boot configures JmsTemplate and listeners from a single RedissonConnectionFactory bean.

Most EMS settings have a counterpart, set per queue or topic on the connection factory:

On EMSOn Redisson PRO
TibjmsConnectionFactoryRedissonConnectionFactory
maxRedelivery on a queuedeliveryLimit in QueueConfig (default 10)
$sys.undelivered, opted into per messagedeadLetterQueueName per queue and deadLetterTopicName per subscription, for every message that reaches the delivery limit or is rejected; expired messages are removed, not dead-lettered
No acknowledgement timeout: an unacknowledged message waits until the session recovers or closesvisibility per queue or subscription, 30 seconds by default; when it expires the message goes back to the queue
maxmsgsmaxSize
Exclusive queueprocessingMode(SEQUENTIAL): one message at a time, in order, whichever consumer polls
Priority and delivery delaysetPriority (0 to 9) and setDeliveryDelay on the producer
Durable subscriptionscreateDurableConsumer, with a client ID set on the factory
Fault-tolerant pair or FTL storesValkey or Redis replicas with Sentinel or Cluster, plus AOF persistence. For confirmed writes set syncMode to ACK or ACK_AOF and syncFailureMode to THROW_EXCEPTION; by default a send that misses the one-second sync timeout only logs a warning. Replication is asynchronous underneath, so a failover can still lose a confirmed write
Destination bridgesA topic with durable subscriptions, so each application gets its own copy
RELIABLE_DELIVERY, NO_ACKNOWLEDGE, explicit acknowledge modesStandard JMS modes; these constants exist only on EMS
import java.time.Duration;
import javax.jms.Connection;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.api.queue.QueueConfig;
import org.redisson.config.Config;
import org.redisson.jms.RedissonConnectionFactory;

Config config = new Config();
// A single server keeps the example short; production uses Sentinel or Cluster
config.useSingleServer().setAddress("redis://valkey:6379");
RedissonClient redisson = Redisson.create(config);

RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson);
factory.setClientId("orders-app");   // required for durable subscriptions
factory.setQueueConfig("orders", QueueConfig.defaults()
        .deliveryLimit(5)            // maxRedelivery=5 on EMS
        // EMS has no acknowledgement timeout. Here an unacknowledged message
        // returns to the queue when this expires, so keep it above the
        // slowest processing time.
        .visibility(Duration.ofMinutes(5))
        .deadLetterQueueName("orders-dlq")); // over-limit and rejected messages

// The JMS code from the EMS example continues from here, minus the EMS-only
// parts: Tibjms.EXPLICIT_CLIENT_ACKNOWLEDGE becomes a standard mode, and
// JMS_TIBCO_PRESERVE_UNDELIVERED is no longer needed. Test the selector.
Connection connection = factory.createConnection();

Replacing EXPLICIT_CLIENT_ACKNOWLEDGE with the standard CLIENT_ACKNOWLEDGE keeps the behavior of the example, which acknowledges each message as soon as it is processed, provided processing finishes within the visibility timeout. EMS holds an unacknowledged message until the session recovers or closes; Redisson, like a Kafka share group, hands it to another consumer once the timeout expires. Code that holds several messages and acknowledges them out of order needs restructuring on any standard provider, for instance one session per message in flight. There is more on the API in JMS messaging over Valkey and Redis, the Jakarta Messaging 3.1 walkthrough and dead letter queues and topics with JMS.

It does not fit every EMS estate:

  • It is a JVM library. EMS's .NET, C and COBOL clients have no counterpart, so mixed-language estates keep a broker for those applications.
  • Code that uses EMS extensions changes on any provider: the Tibjms constants, JMS_TIBCO_* properties, routes and bridges.
  • XA is documented for RMap, RMapCache, RLocalCachedMap, RSet, RSetCache and RBucket through Redisson's XAResource, not for JMS sends. An application that commits a message and a database row together needs that confirmed first.
  • Messages live in memory. Size Valkey or Redis for the deepest queue you expect, enable AOF persistence, and choose an eviction policy that never evicts messaging data. Topics keep every message by default (SUBSCRIPTION_OPTIONAL_RETAIN_ALL), which is what makes replay possible and also what fills memory. Give a topic a timeToLive that matches the history you need, or use SUBSCRIPTION_REQUIRED_DELETE_PROCESSED where nobody replays, as on a topic that replaces an EMS bridge. A maxSize only caps the topic: once it is full, publishing blocks or adds nothing.

If Replay Is the Reason for Kafka

Reliable PubSub, the object behind Redisson PRO's JMS topics, keeps messages after they are consumed. By default a topic stores every message, whether or not any subscription has processed it, with no expiry, and each subscription keeps its own offset. A subscription created with Position.earliest() starts from the oldest message the topic still holds, and seek() moves an existing one to the earliest or latest message, a message ID or a timestamp. Inside a subscription, consumers share the messages and acknowledge each one, with the visibility timeout, delivery limit and dead-letter topic from the table above. One topic can therefore feed the applications that need queue semantics and a new consumer that reads the history from the start, the combination that otherwise takes EMS, Kafka and EMS 10.5's Kafka transport between them.

It stops short of Kafka in three places:

  • The history is held in Valkey or Redis memory, where Kafka keeps it on disk, so how far back you can replay depends on how much RAM you pay for. A timeToLive on the topic sets the window. To rewind a subscription over messages it has already acknowledged, create the subscription with retainAfterAck(); without it, the subscription discards each message it acknowledges.
  • Replay belongs to Redisson's own API, because JMS has no call to rewind a subscription. The messaging documentation creates a subscription at Position.earliest() through that API and registers it on the connection factory, but its JNDI properties have no position setting.
  • There is no stream-processing library to match Kafka Streams, so windows, joins and aggregations are application code.

Apache Kafka vs Reliable PubSub compares the two feature by feature.

How to Choose

If this describes youThen
.NET, C or COBOL applications share the EMS server with Java onesStay on EMS for now. Redisson is JVM-only, and Kafka means rewriting every one of those clients
An EMS queue and a database commit together under XAStay on EMS, or budget an outbox redesign for Kafka
You need stream processing, or replay over more history than you can keep in memoryKafka. EMS keeps no history at any setting, and Redisson PRO keeps its history in Valkey or Redis memory
Replay for new consumers and queue semantics for existing onesRedisson PRO if every client runs on the JVM and the history fits in memory: one Reliable PubSub topic does both. Otherwise EMS and Kafka, bridged with EMS 10.5's Kafka transport
Java-only JMS queues and topics, licence or operations are the problem, and Valkey or Redis is already in the stackRedisson PRO (commercial licence): a JMS provider without a separate broker process, provided Valkey or Redis runs with persistence and replicas

Most estates land in more than one row, which is a reason to plan the migration workload by workload rather than system by system. For the wider field of options, see Apache Kafka alternatives and RabbitMQ alternatives.

Frequently Asked Questions

What is EMS in TIBCO?

EMS stands for Enterprise Message Service, TIBCO's JMS message broker. It runs as a server that applications reach through JMS on Java or through TIBCO's client libraries for .NET, C and COBOL, and it keeps messages in queues and topics until they are consumed. The current release is 10.5.1, from September 2026.

Is TIBCO outdated?

TIBCO still develops EMS. Version 10.5.0 shipped in February 2026 with a native transport for Apache Kafka, and 10.5.1 followed in September 2026 with changes such as filtering for ObjectMessage deserialization. It runs on a commercial support calendar: TIBCO ended support for 10.3.0 on March 31, 2026, so staying current means regular upgrades. EMS has no replay or long retention, and that gap is usually what sends teams to look at Kafka.

Is TIBCO still in business?

Yes. TIBCO Software is part of Cloud Software Group, formed in 2022 when Citrix was taken private and combined with TIBCO, and it continues to release EMS.

Can Kafka replace TIBCO EMS?

For some workloads. Kafka replaces EMS well where consumers benefit from replay, volumes are high and ordering per partition is enough. It replaces EMS badly where applications rely on selectors, XA with a database, message priority, delayed delivery, or EMS's own modes such as RELIABLE_DELIVERY and EXPLICIT_CLIENT_ACKNOWLEDGE. Kafka is also not a JMS provider, so every JMS producer and consumer is rewritten. Since Kafka 4.2, share groups acknowledge records individually, and since 4.4 they can write failed records to a dead-letter topic, which narrows the gap for plain queue workloads.

When to use JMS vs Kafka?

Use JMS, through EMS or another provider, when each message is a unit of work for one consumer, when you need per-message acknowledgement, redelivery limits, selectors or XA, and when your code is already written against the JMS API. Use Kafka when several systems read the same stream independently, when you need to replay history, or when throughput and retention matter more than per-message control. Many organizations run both.

What are the TIBCO EMS delivery modes?

There are three. PERSISTENT messages sent to a queue are written to the server's store, as are those published to a topic that has a durable subscriber or a subscriber with a fault-tolerant connection. NON_PERSISTENT messages are not stored. RELIABLE_DELIVERY, an EMS extension, is not stored either, and the server never sends the producer a receipt, so a send succeeds unless the connection has failed. Kafka has no equivalent of a message that is never stored; every record is written to the log.

Does Kafka support JMS?

No. Kafka has its own client API and is not a JMS provider. Confluent's JMS 1.1 client for Kafka, which never supported selectors, durable subscribers, transactions or temporary destinations, was removed in Confluent Platform 8.0 in June 2025. JMS code that moves to Kafka is rewritten against the Kafka API.

Is there anything better than Kafka?

It depends on the job. For stream processing and long replayable history, Kafka is the default, and its main rivals are Kafka-compatible platforms and Apache Pulsar. For queue work, which is most of what EMS does, a JMS provider or a queue built for per-message delivery usually fits better than a log. Where every client runs on the JVM and the history fits in memory, Redisson PRO's Reliable PubSub on Valkey or Redis handles queue work and replay in one topic: consumers in a subscription acknowledge messages one at a time, and a subscription can start from or seek to a message ID or a timestamp. Our Apache Kafka alternatives comparison sorts the options by the job they do.

Similar articles