Skip to main content

Azure Event Hubs

Subscribes to an Azure Event Hub and forwards each event to the pipeline. Any publisher speaking the Event Hubs protocol works, including Microsoft Fabric Real-Time Intelligence eventstreams through their Event Hubs-compatible endpoint.

When to use it

  • Your telemetry already lands in Azure — from Azure IoT Hub, which exposes an Event Hubs-compatible endpoint, from a Fabric eventstream, or from producers writing to a hub directly.
  • You want the buffering and fan-out an event broker gives you between the devices and the scene.

Install

npm install @azure/event-hubs

Configuration

sources: [
{
kind: "azure-event-hub",
config: {
connection_string: process.env.EVENT_HUB_CONNECTION_STRING!,
consumer_group: "livelink",
},
},
],
OptionTypeDefaultDescription
connection_stringstringRequired. Usually embeds an EntityPath= segment
consumer_groupstring$DefaultThe consumer group to read with
event_hub_namestringOnly needed when the connection string does not embed EntityPath=

A connection string looks like this:

Endpoint=sb://xxxx.servicebus.windows.net/;SharedAccessKeyName=key_xxxx;SharedAccessKey=xxxx;EntityPath=xxxx

It is a credential — keep it in the environment, never in the source. The key is redacted before the transport logs anything.

What arrives

{
channel: "3", // the PARTITION ID — see below
payload: { deviceId: "sensor-14", temperature: 21.4 },
received_at: Date,
source_timestamp: Date, // enqueuedTimeUtc
metadata: {
transport: "azure-event-hub",
partition_id: "3",
offset: "12345",
sequence_number: 987,
},
}

Event Hubs delivers bodies already deserialized. Non-object bodies (a raw string, a number) are logged and skipped, so the pipeline always sees a stable shape.

Never select on channel here

This is the one thing that will otherwise cost you a day.

Event Hubs has no per-message routing key. Unlike an MQTT topic, a partition is a load-balancing artifact: the same logical device can land on any partition, and the assignment can change. A mapping that selects channel: "3" works until it silently stops.

Select on the payload instead:

const temperatureMapping: EventMapping = {
// NOT channel — the payload says what this event is.
when: event => (event.payload as { temperature?: unknown }).temperature !== undefined,

entities: { byName: "sensor-{id}" },

updates: event => {
const { deviceId, temperature } = event.payload as { deviceId: string; temperature: number };
return { id: deviceId, update: { local_transform: { scale: [1, temperature / 50, 1] } } };
},
};

The partition id, offset and sequence number remain available in metadata — useful for diagnostics, not for routing.

Positions and checkpointing

The transport reads from the latest position and does not checkpoint. It picks up events that arrive after it connects, and a restart does not replay what it missed.

For driving a live scene that is usually what you want: on reconnecting you care about the current state of the plant, not the backlog. It does mean the transport is not a fit for anything that must not miss an event.

If you need durable positions, own the client yourself and point it at the ingestion — it is an EventSink:

import { EventHubConsumerClient } from "@azure/event-hubs";
import { BlobCheckpointStore } from "@azure/eventhubs-checkpointstore-blob";

const checkpoint_store = new BlobCheckpointStore(containerClient);
const client = new EventHubConsumerClient(consumer_group, connection_string, checkpoint_store);

client.subscribe({
processEvents: async (events, context) => {
for (const event of events) {
await ingestion.ingest({
channel: context.partitionId,
payload: event.body,
source_timestamp: event.enqueuedTimeUtc,
});
}
await context.updateCheckpoint(events[events.length - 1]);
},
processError: async error => console.error(error),
});

The sequence_number and offset carried in each event's metadata are exactly what such a store records. See writing your own transport for the general shape of this.

Ordering

Event Hubs guarantees ordering within a partition, not across them. Two events about the same device are ordered only if the producer gives them the same partition key.

For scene ingestion this rarely matters — you are writing current state, and the pipeline skips redundant writes anyway. It does matter if a mapping accumulates rather than overwrites (integrating a rate into an angle, say). Key such a stream by device at the producer.