An Azure event routing service designed for high availability, consistent performance, and dynamic scale.
Hello @Sandeep Sureshkumar
What you're observing is understandable, but there is an important distinction between IoT Hub message routing to the built-in endpoint and IoT Hub → Event Grid integration.
Your two queries themselves are mutually exclusive:
Event Grid:
NOT(is_defined(appTopic) AND starts_with(appTopic, "rt.test/dev"))
Built-in events:
is_defined(appTopic) AND starts_with(appTopic, "rt.test/dev")
So, based purely on normal IoT Hub routing, rt.test/fnd/... should match only the first condition, while rt.test/dev/... should match only the second.
However, Event Grid integration works differently than a normal custom message-routing endpoint.
When you create an Event Grid subscription for Microsoft.Devices.DeviceTelemetry, IoT Hub creates a default route named RouteToEventGrid based on that subscription. Microsoft specifically documents that if you want to filter telemetry before it's published to Event Grid, you modify that routing query.
The built-in events endpoint and fallback routing have another behavior worth noting. Microsoft documents that once custom routes exist, messages only reach the built-in endpoint through an explicit route to events or through the fallback mechanism. The fallback route is intended for messages that don't match another applicable message route.
In a standard routing configuration, a message can also match multiple routes. If it does, IoT Hub sends it to each associated endpoint; it only deduplicates when multiple matching routes have the same destination.
That makes your A/B test particularly useful:
Fallback disabled
rt.test/dev/... → built-in endpoint
rt.test/fnd/... → Event Grid
This is exactly what your routing conditions imply.
Fallback enabled
rt.test/dev/... → built-in endpoint
rt.test/fnd/... → Event Grid + built-in endpoint
The second result isn't what I'd expect from the documented definition of fallback routing if rt.test/fnd/... has successfully matched RouteToEventGrid. Microsoft describes fallback as handling messages that don't match any routes when fallback is enabled.
So I wouldn't explain this simply as "fallback always sends a copy to the built-in endpoint." That's not the documented behavior.
What I would check
First, go to: IoT Hub → Message routing → Routes
and verify the actual route generated for Event Grid, particularly the route named something similar to: RouteToEventGrid
Confirm that the condition there is exactly: NOT(is_defined(appTopic) AND starts_with(appTopic, "rt.test/dev"))
Also use the Test route with both sample messages. Microsoft provides this specifically for verifying whether a message matches a routing query.
I'd also check the IoT Hub routing metrics, especially Routing Deliveries and related routing metrics. Microsoft recommends these when diagnosing whether messages matched routes, were delivered, or were handled by fallback.
If the route tester/metrics confirm rt.test/fnd/... matches RouteToEventGrid, but enabling fallback still causes an additional copy to appear on $Default/events, then I think you've isolated behavior that deserves investigation by the IoT Hub team.
Your test is actually quite strong because changing only the fallback setting changes whether the duplicate reaches the built-in endpoint.
I would provide Microsoft Support with the IoT Hub name/region, Event Grid subscription, RouteToEventGrid configuration, timestamps/message IDs, routing metrics, and the two test results showing fallback enabled versus disabled.
Sharing these references with you:
Microsoft - Azure IoT Hub and Event Grid
Microsoft - Understand IoT Hub message routing
Microsoft - Troubleshoot Azure IoT Hub message routing
So, based on Microsoft's documented routing semantics, I wouldn't consider the duplicate delivery to events automatically expected simply because fallback is enabled. If the Event Grid route genuinely matched the message, fallback shouldn't normally be required for that message. Your fallback-on/off reproduction is worth escalating if the route tester confirms the match.
Help make this community better for everyone: if this answer resolved your issue, please accept it or leave an upvote. If not, share more details in a comment so we can continue the discussion and find the right solution.