Skip to content

Key-Shared Subscriptions

Key-Shared subscriptions route messages to consumers based on their routing key. All messages with the same key go to the same consumer, in order. This combines the parallelism of Shared subscriptions with per-key ordering guarantees.

RequirementSubscription Type
Total ordering, single consumerExclusive
Maximum throughput, no orderingShared
Ordering + high availabilityFailover
Per-key ordering + parallelismKey-Shared ✓

Key-Shared is the right choice when you need per-key ordering with multi-consumer parallelism, key-affinity processing (all events for a given entity go to the same consumer), or selective routing via key filters.


Tag each message with a routing key using send_with_key(). Messages with the same key are always delivered to the same consumer.

Send with routing keys
let mut producer = client
.new_producer()
.with_topic("/default/orders")
.with_name("orders_producer")
.with_reliable_dispatch()
.build()?;
producer.create().await?;
// All "payment" messages go to the same consumer
producer.send_with_key(b"Payment for #1001".to_vec(), None, "payment").await?;
// "shipping" messages go to (potentially) a different consumer
producer.send_with_key(b"Order #1001 shipped".to_vec(), None, "shipping").await?;
// Same key = same consumer, guaranteed
producer.send_with_key(b"Payment for #1002".to_vec(), None, "payment").await?;
  • send_with_key(payload, attributes, routing_key) tags the message with a routing key
  • Messages with the same routing key always go to the same consumer
  • If you use send() instead, the producer name is used as the fallback routing key
  • Routing keys can be any string: order IDs, user IDs, device IDs, event types

The simplest Key-Shared consumer uses automatic key distribution via consistent hashing. The broker assigns keys to consumers automatically.

Key-Shared consumer
let mut consumer = client
.new_consumer()
.with_topic("/default/orders")
.with_consumer_name("worker_1")
.with_subscription("orders_sub")
.with_subscription_type(SubType::KeyShared)
.build()?;
consumer.subscribe().await?;
let mut stream = consumer.receive().await?;
while let Some(message) = stream.recv().await {
let key = message.routing_key.as_deref().unwrap_or("<none>");
let payload = String::from_utf8_lossy(&message.payload);
println!("key={:<10} | '{}'", key, payload);
consumer.ack(&message).await?;
}

Start multiple consumers with the same subscription name. Each consumer receives a distinct subset of routing keys:

Expected output
# consumer_1 output
key=shipping | 'Order #1001 shipped via express'
key=shipping | 'Order #1002 shipped via standard'
# consumer_2 output
key=payment | 'Payment received for order #1001'
key=payment | 'Payment received for order #1002'
key=invoice | 'Invoice generated for order #1001'

The exact key-to-consumer assignment depends on consistent hashing but is deterministic.


Key filtering gives consumers explicit control over which keys they handle. Each consumer declares glob patterns and only receives messages whose routing key matches.

Filtered consumer
// Only receives "payment" and "invoice" messages
let mut consumer = client
.new_consumer()
.with_topic("/default/orders")
.with_consumer_name("payments_worker")
.with_subscription("orders_filtered")
.with_subscription_type(SubType::KeyShared)
.with_key_filter("payment")
.with_key_filter("invoice")
.build()?;

Filters use glob syntax:

PatternMatches
"payment"Exact match only
"ship*""shipping", "shipment", etc.
"eu-west-?""eu-west-1", "eu-west-2", etc.
"*"Everything (same as no filter)

You can also set multiple filters at once:

Multiple filters
// Chain individual filters
.with_key_filter("payment")
.with_key_filter("invoice")
// Or set multiple at once
.with_key_filters(vec!["payment".into(), "invoice".into()])
Two filtered consumers
# payments consumer (filters: "payment", "invoice")
key=payment | 'Payment received for order #1001'
key=payment | 'Payment received for order #1002'
key=invoice | 'Invoice generated for order #1001'
# logistics consumer (filters: "ship*", "return")
key=shipping | 'Order #1001 shipped via express'
key=return | 'Return request for order #998'
key=shipping | 'Order #1002 shipped via standard'

At most one message per routing key is in-flight at any time. If a second message arrives for a key that already has an unacknowledged message, it is queued until the first is acked.

When consumers join or leave, key-to-consumer mappings are rebalanced using consistent hashing. Only approximately 1/N of existing key assignments are remapped. The rest remain stable.

If a message’s routing key matches no consumer’s filters, the message is skipped by that subscription. The offset advances past it. The message still remains in the topic for other subscriptions.

Key-Shared supports the same ack/nack semantics as other reliable subscription types. See the Consumer Guide for ack/nack details.


Route all events for the same order to the same consumer:

producer.send_with_key(data, None, &format!("order-{}", order_id)).await?;

Dedicate consumers to specific tenants using key filtering:

.with_key_filter("tenant-a-*") // Tenant A consumer

Route device telemetry by device ID for per-device state tracking:

producer.send_with_key(data, None, &format!("device-{}", device_id)).await?;