MQTT Topic Engine
High-performance MQTT topic pattern matching and routing engine for Rust

Zero-dependency (by default) topic pattern parser, matcher, and router for MQTT applications
This crate is the topic-routing engine behind mqtt-typed-client.
If you want a type-safe async MQTT client — connect, typed publish/subscribe, automatic
topic routing and (de)serialization — reach for
mqtt-typed-client; that's what most users
want. This crate is the matching/routing core on its own: dependency-light and
client-agnostic, for when you're building your own MQTT layer and just need the matcher.
Contents
Features
- Pattern Parsing - Parse MQTT topic patterns with support for wildcards (
+, #) and named parameters
- Fast Matching - Efficient tree-based topic matching algorithm with O(n) complexity
- Optional Caching - LRU cache for topic match results to boost performance (optional
lru-cache feature)
- Message Routing - Route incoming messages to multiple subscribers based on topic patterns (optional
router feature)
- Named Parameters - Extract topic segments as named parameters (
{sensor_id}, {path:#})
- Modular Design - Enable only the features you need for minimal binary size
- Well Tested - Comprehensive test suite with 100+ tests covering edge cases
- Lightweight - Minimal dependencies, optimized for embedded and performance-critical applications
- MQTT Client Agnostic - Works with any MQTT client library (rumqttc, paho-mqtt, ntex-mqtt, etc.)
Installation
Add to your Cargo.toml:
[dependencies]
mqtt-topic-engine = "0.1.0"
Feature Flags
The library is highly modular with optional features:
mqtt-topic-engine = "0.1.0"
mqtt-topic-engine = { version = "0.1.0", default-features = false }
mqtt-topic-engine = { version = "0.1.0", default-features = false, features = ["router"] }
mqtt-topic-engine = { version = "0.1.0", default-features = false, features = ["lru-cache"] }
mqtt-topic-engine = { version = "0.1.0", features = ["rumqttc"] }
mqtt-topic-engine = { version = "0.1.0", features = ["paho-mqtt"] }
mqtt-topic-engine = { version = "0.1.0", features = ["ntex-mqtt"] }
Available features:
| Feature |
Description |
Default |
router |
Topic routing with TopicRouter and TopicMatcher |
Yes |
lru-cache |
LRU caching for pattern matching results |
Yes |
rumqttc |
Integration with rumqttc MQTT client (QoS conversion) |
No |
paho-mqtt |
Integration with paho-mqtt client (QoS conversion) |
No |
ntex-mqtt |
Integration with ntex-mqtt client (QoS conversion) |
No |
Quick Start
Basic Pattern Matching
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"sensors/+/temperature",
CacheStrategy::NoCache
)?;
let topic_match = pattern.try_match_str("sensors/kitchen/temperature")?;
println!("Topic matched: {}", topic_match.topic_path());
# Ok(())
# }
Named Parameters
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"devices/{location}/{device_id}/status",
CacheStrategy::NoCache
)?;
let topic_match = pattern.try_match_str("devices/kitchen/sensor001/status")?;
let location = topic_match.get_named_param("location").unwrap();
let device_id = topic_match.get_named_param("device_id").unwrap();
println!("Location: {}, Device: {}", location, device_id);
# Ok(())
# }
Multi-Level Wildcards
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"logs/{service}/{details:#}",
CacheStrategy::NoCache
)?;
let topic_match = pattern.try_match_str("logs/api/v1/users/create")?;
let service = topic_match.get_named_param("service").unwrap();
let details = topic_match.get_named_param("details").unwrap();
println!("Service: {}, Path: {}", service, details);
# Ok(())
# }
Core Concepts
Topic Patterns
MQTT topic engine supports standard MQTT wildcards plus named parameters:
| Pattern |
Description |
Example |
Matches |
sensors/temperature |
Exact match |
sensors/temperature |
sensors/temperature only |
sensors/+/data |
Single-level wildcard |
sensors/+/data |
sensors/kitchen/data, sensors/bedroom/data |
sensors/# |
Multi-level wildcard |
sensors/# |
sensors, sensors/temp, sensors/kitchen/temp |
sensors/{room}/temp |
Named parameter |
sensors/{room}/temp |
Same as sensors/+/temp but extracts room |
logs/{app}/{path:#} |
Named multi-level |
logs/{app}/{path:#} |
Same as logs/+/# but extracts both |
Topic Router
Route messages to multiple subscribers based on patterns:
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, TopicPath, CacheStrategy, QoS};
let mut router = TopicRouter::new();
let pattern1 = TopicPatternPath::new_from_string("sensors/+/temperature", CacheStrategy::NoCache)?;
let (needs_subscribe, sub_id1) = router.add_subscription(
pattern1,
QoS::AtLeastOnce,
"handler1" );
let pattern2 = TopicPatternPath::new_from_string("sensors/#", CacheStrategy::NoCache)?;
let (needs_subscribe, sub_id2) = router.add_subscription(
pattern2,
QoS::AtMostOnce,
"handler2"
);
let topic = TopicPath::new("sensors/kitchen/temperature");
let subscribers = router.get_subscribers(&topic);
for (sub_id, (pattern, _qos), handler) in subscribers {
println!("Matched subscriber: {:?} with pattern: {}", sub_id, pattern.topic_pattern());
}
# let _ = (needs_subscribe, sub_id1, sub_id2);
# Ok(())
# }
Parameter Binding
Bind specific values to parameters for filtered subscriptions:
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"sensors/{location}/{sensor_type}/{sensor_id}/data",
CacheStrategy::NoCache
)?;
let filtered = pattern
.bind_parameter("location", "kitchen")?
.bind_parameter("sensor_type", "temperature")?;
println!("{}", filtered.mqtt_pattern());
# Ok(())
# }
This is useful for:
- Dynamic subscription filtering
- Multi-tenant applications
- Selective message routing
Advanced Examples
LRU Caching for Performance
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
use std::num::NonZeroUsize;
let cache_size = NonZeroUsize::new(1000).unwrap();
let pattern = TopicPatternPath::new_from_string(
"sensors/{location}/{device}/data",
CacheStrategy::Lru(cache_size)
)?;
let match1 = pattern.try_match_str("sensors/kitchen/temp001/data")?;
let match2 = pattern.try_match_str("sensors/kitchen/temp001/data")?;
# let _ = (match1, match2);
# Ok(())
# }
Topic Pattern Validation
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy, TopicPatternError};
assert!(TopicPatternPath::new_from_string("sensors/+/data", CacheStrategy::NoCache).is_ok());
assert!(TopicPatternPath::new_from_string("logs/#", CacheStrategy::NoCache).is_ok());
assert!(TopicPatternPath::new_from_string("home/{room}/temperature", CacheStrategy::NoCache).is_ok());
assert!(matches!(
TopicPatternPath::new_from_string("sensors/#/invalid", CacheStrategy::NoCache),
Err(TopicPatternError::HashPosition { .. })
));
assert!(matches!(
TopicPatternPath::new_from_string("", CacheStrategy::NoCache),
Err(TopicPatternError::EmptyTopic)
));
Building Topics for Publishing
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"devices/{device_type}/{device_id}/command",
CacheStrategy::NoCache
)?;
let topic = pattern.format_topic(&[&"sensor", &42])?;
println!("{}", topic);
# Ok(())
# }
Managing Subscriptions
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, CacheStrategy, QoS};
let mut router = TopicRouter::new();
let pattern = TopicPatternPath::new_from_string("sensors/+/data", CacheStrategy::NoCache)?;
let (needs_mqtt_subscribe, sub_id) = router.add_subscription(
pattern,
QoS::AtLeastOnce,
"my_handler"
);
if needs_mqtt_subscribe {
println!("New subscription needed on broker");
}
let (needs_mqtt_unsubscribe, pattern) = router.unsubscribe(&sub_id)?;
if needs_mqtt_unsubscribe {
println!("Should unsubscribe from broker: {}", pattern.mqtt_pattern());
}
# Ok(())
# }
QoS Aggregation
A TopicRouter is more than a pattern -> handler map: it tracks the maximum
QoS requested across all subscribers of the same pattern and tells you, via the
needs_subscribe flag, exactly when a broker action is required. You only need to
(re)subscribe on the broker when a pattern is new or when its aggregated QoS rises —
adding another subscriber at the same or a lower QoS needs no wire traffic at all.
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, CacheStrategy, QoS};
let mut router = TopicRouter::new();
let pattern = || TopicPatternPath::new_from_string("sensors/+/data", CacheStrategy::NoCache);
let (needs_subscribe, _id1) = router.add_subscription(pattern()?, QoS::AtMostOnce, "h1");
assert!(needs_subscribe);
let (needs_subscribe, _id2) = router.add_subscription(pattern()?, QoS::AtLeastOnce, "h2");
assert!(needs_subscribe);
let (needs_subscribe, _id3) = router.add_subscription(pattern()?, QoS::AtMostOnce, "h3");
assert!(!needs_subscribe);
# Ok(())
# }
Wildcard Patterns
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, TopicPath, CacheStrategy};
use std::sync::Arc;
let pattern1 = TopicPatternPath::new_from_string("home/+/temperature", CacheStrategy::NoCache)?;
let pattern2 = TopicPatternPath::new_from_string("home/{room}/temperature", CacheStrategy::NoCache)?;
let topic = Arc::new(TopicPath::new("home/kitchen/temperature"));
let match1 = pattern1.try_match(topic.clone())?;
let match2 = pattern2.try_match(topic)?;
assert!(match2.get_named_param("room").is_some());
println!("Room: {}", match2.get_named_param("room").unwrap());
# let _ = match1;
# Ok(())
# }
Complex Routing Scenarios
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, TopicPath, CacheStrategy, QoS};
let mut router = TopicRouter::<String>::new();
let patterns = vec![
("home/kitchen/temperature", "exact_handler"),
("home/+/temperature", "any_room_temp_handler"),
("home/kitchen/+", "kitchen_all_sensors_handler"),
("home/#", "all_home_handler"),
("+/kitchen/#", "all_kitchen_subtopics_handler"),
];
for (pattern_str, handler_name) in patterns {
let pattern = TopicPatternPath::new_from_string(pattern_str, CacheStrategy::NoCache)?;
router.add_subscription(pattern, QoS::AtMostOnce, handler_name.to_string());
}
let topic = TopicPath::new("home/kitchen/temperature");
let subscribers = router.get_subscribers(&topic);
println!("Message 'home/kitchen/temperature' matches {} subscribers:", subscribers.len());
for (sub_id, (pattern, _qos), handler) in subscribers {
println!(" - {} (pattern: {})", handler, pattern.topic_pattern());
let _ = sub_id;
}
# Ok(())
# }
Reconnection: Resubscribe After Broker Restart
When the broker connection drops, you must replay your subscriptions. The router
gives you the deduplicated set of patterns, each already collapsed to the
maximum QoS among its subscribers, so you resubscribe each distinct topic
exactly once at the correct level:
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, CacheStrategy, QoS};
let mut router = TopicRouter::new();
let pattern = |s| TopicPatternPath::new_from_string(s, CacheStrategy::NoCache);
router.add_subscription(pattern("sensors/+/data")?, QoS::AtMostOnce, "h1");
router.add_subscription(pattern("sensors/+/data")?, QoS::ExactlyOnce, "h2");
router.add_subscription(pattern("alerts/#")?, QoS::AtLeastOnce, "h3");
let to_resubscribe = router.get_topics_for_resubscribe();
assert_eq!(to_resubscribe.len(), 2);
assert_eq!(to_resubscribe.get("sensors/+/data").copied(), Some(QoS::ExactlyOnce));
assert_eq!(to_resubscribe.get("alerts/#").copied(), Some(QoS::AtLeastOnce));
for (topic_pattern, qos) in &to_resubscribe {
let _ = (topic_pattern, qos);
}
# Ok(())
# }
Integration with MQTT Clients
With rumqttc
use mqtt_topic_engine::{TopicRouter, TopicPatternPath, TopicPath, CacheStrategy};
use rumqttc::{AsyncClient, MqttOptions, QoS, Event, Packet};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mqtt_options = MqttOptions::new("test-client", "broker.hivemq.com", 1883);
let (client, mut eventloop) = AsyncClient::new(mqtt_options, 10);
let mut router = TopicRouter::<String>::new();
let pattern = TopicPatternPath::new_from_string("sensors/+/data", CacheStrategy::NoCache)?;
let (needs_subscribe, _sub_id) = router.add_subscription(
pattern.clone(),
QoS::AtLeastOnce,
"sensor_handler".to_string()
);
if needs_subscribe {
client.subscribe(pattern.mqtt_pattern().as_ref(), QoS::AtLeastOnce).await?;
}
loop {
match eventloop.poll().await {
Ok(Event::Incoming(Packet::Publish(publish))) => {
let topic = TopicPath::new(publish.topic.as_str());
let subscribers = router.get_subscribers(&topic);
for (_sub_id, (pattern, _qos), handler) in subscribers {
println!("Handler '{}' received message on pattern: {}",
handler, pattern.topic_pattern());
}
}
Ok(_) => {}
Err(e) => {
eprintln!("Error: {:?}", e);
break;
}
}
}
Ok(())
}
Use Cases
IoT Device Management
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"iot/{building}/{floor}/sensors/{sensor_type}/{sensor_id}/telemetry",
CacheStrategy::NoCache
)?;
let filtered = pattern
.bind_parameter("building", "headquarters")?
.bind_parameter("floor", "3")?;
println!("{}", filtered.mqtt_pattern());
# Ok(())
# }
Log Aggregation
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"logs/{service}/{severity}/{details:#}",
CacheStrategy::NoCache
)?;
let topic_match = pattern.try_match_str("logs/api/error/auth/invalid-token")?;
let service = topic_match.get_named_param("service").unwrap();
let severity = topic_match.get_named_param("severity").unwrap();
let details = topic_match.get_named_param("details").unwrap();
if severity == "error" {
println!("ERROR in {}: {}", service, details);
}
# Ok(())
# }
Multi-Tenant Applications
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"tenant/{tenant_id}/devices/{device_id}/events",
CacheStrategy::NoCache
)?;
let tenant_a_pattern = pattern.clone().bind_parameter("tenant_id", "tenant-a")?;
let tenant_b_pattern = pattern.bind_parameter("tenant_id", "tenant-b")?;
# let _ = (tenant_a_pattern, tenant_b_pattern);
# Ok(())
# }
Working with ArcStr
Under the hood, mqtt-topic-engine uses ArcStr for efficient string handling with cheap cloning.
You can pass regular &str or String values, and they will be converted automatically:
# fn main() {
use mqtt_topic_engine::TopicPath;
use arcstr::ArcStr;
let topic1 = TopicPath::new("sensors/temp"); let topic2 = TopicPath::new(String::from("sensors/temp")); let topic3 = TopicPath::new(ArcStr::from("sensors/temp"));
let topic_string = ArcStr::from("sensors/temperature");
let topic_a = TopicPath::new(topic_string.clone()); let topic_b = TopicPath::new(topic_string.clone()); let topic_c = TopicPath::new(topic_string); # let _ = (topic1, topic2, topic3, topic_a, topic_b, topic_c);
# }
When to use ArcStr directly:
- Storing topic strings for reuse (cheap cloning via reference counting)
- High-frequency topic creation from the same string values
- Sharing topic strings across threads (
ArcStr is Send + Sync)
Not needed for simple one-off topic matching — just use &str, it's simpler.
Configuration
Cache Strategies
# fn main() {
use mqtt_topic_engine::CacheStrategy;
use std::num::NonZeroUsize;
let no_cache = CacheStrategy::NoCache;
let lru_100 = CacheStrategy::Lru(NonZeroUsize::new(100).unwrap());
let lru_10k = CacheStrategy::Lru(NonZeroUsize::new(10000).unwrap());
# let _ = (no_cache, lru_100, lru_10k);
# }
When to use caching:
- Repeated matches to the same topics (e.g., sensor data every second)
- High message throughput
- Complex patterns with multiple wildcards
Skip caching for unique (non-repeating) topics — no cache benefit — and in memory-constrained environments.
Minimal Configuration for Embedded Systems
For resource-constrained environments, use only the core pattern matching without routing or caching:
[dependencies]
mqtt-topic-engine = { version = "0.1.0", default-features = false }
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use mqtt_topic_engine::{TopicPatternPath, CacheStrategy};
let pattern = TopicPatternPath::new_from_string(
"sensors/{location}/data",
CacheStrategy::NoCache )?;
let topic_match = pattern.try_match_str("sensors/kitchen/data")?;
if let Some(location) = topic_match.get_named_param("location") {
# let _ = location;
}
# Ok(())
# }
Benefits of minimal configuration:
- Smallest binary size (~10KB additional code)
- No heap allocations for routing or caching
- Suitable for microcontrollers and embedded systems
- Still provides full pattern matching and parameter extraction
Performance
Topic engine is designed for high-performance applications:
- Matching: O(n) where n = number of topic segments
- Routing: O(m) where m = number of matching subscriptions
- Memory: Optimized with
arcstr and smallvec for minimal allocations
- Caching: Optional LRU cache for repeated topic matches
Expected performance characteristics:
- Pattern parsing: ~1-2 μs per pattern (typical case: 3-5 segments)
- Topic matching: ~200-500 ns per match without cache
- Topic matching: ~20-50 ns with cache hit (LRU lookup)
- Router lookup: ~500 ns - 2 μs (depends on subscription count and matches)
Note: Actual performance depends on pattern complexity, topic depth, and hardware.
Always benchmark with your specific workload.
Integration
MQTT Client Support
The library includes its own QoS type and provides optional integration with popular MQTT clients through seamless QoS type conversions:
Supported MQTT clients:
- rumqttc - Enable with
features = ["rumqttc"]
- paho-mqtt - Enable with
features = ["paho-mqtt"]
- ntex-mqtt - Enable with
features = ["ntex-mqtt"]
- Zero-dependency - Works standalone without any MQTT client (default)
mqtt-topic-engine = { version = "0.1.0", features = ["rumqttc"] }
mqtt-topic-engine = { version = "0.1.0", features = ["paho-mqtt"] }
mqtt-topic-engine = { version = "0.1.0", features = ["ntex-mqtt"] }
mqtt-topic-engine = "0.1.0"
The core matching and routing logic is already client-agnostic and works with any MQTT client library.
QoS Type Conversions
When MQTT client integration features are enabled, the library provides automatic QoS type conversions:
use mqtt_topic_engine::QoS;
#[cfg(feature = "rumqttc")]
let qos: QoS = rumqttc::QoS::AtLeastOnce.into();
#[cfg(feature = "paho-mqtt")]
let qos: QoS = paho_mqtt::QoS::AtLeastOnce.into();
#[cfg(feature = "ntex-mqtt")]
let qos: QoS = ntex_mqtt::QoS::AtLeastOnce.into();
let engine_qos = QoS::AtLeastOnce;
#[cfg(feature = "rumqttc")]
let rumqttc_qos: rumqttc::QoS = engine_qos.to_rumqttc();
#[cfg(feature = "paho-mqtt")]
let paho_qos: paho_mqtt::QoS = engine_qos.to_paho_mqtt();
#[cfg(feature = "ntex-mqtt")]
let ntex_qos: ntex_mqtt::QoS = engine_qos.to_ntex_mqtt();
This enables seamless integration with any supported MQTT client without manual QoS type conversions.
API Documentation
For detailed API documentation, visit docs.rs/mqtt-topic-engine.
Alternatives
Standalone MQTT topic-matching crates are rare — matching is usually embedded
inside a full client or broker rather than offered as a reusable building block:
mqtt_topic_tree — a lightweight
topic tree for MQTT routing, and the closest standalone equivalent. This engine
adds named-parameter extraction, pluggable LRU caching, a typed QoS with
conversions to/from popular clients, and a zero-dependency embedded mode.
- The matching built into full clients and brokers (e.g. rumqttc, ntex-mqtt) —
reach for those when you don't need a reusable, client-agnostic matcher.
Choose mqtt-topic-engine when you want MQTT wildcard matching with parameter
extraction as a small, client-agnostic component — usable with rumqttc, paho-mqtt,
ntex-mqtt, or no MQTT client at all.
Contributing
Contributions are welcome! This library is part of the mqtt-typed-client project.
License
This project is licensed under either of
at your option.
See Also