Azure Integration

Azure Functions Integration Triggers: Service Bus, Event Grid and Event Hubs Bindings in Practice

You wire an Azure Functions app to a Service Bus queue, it works, and you assume the next integration will be the same. Then you point a function at an Event Grid topic and nothing fires — the subscription sits at AwaitingValidation forever. You switch a third to Event Hubs and it processes fine in test, then in production falls hours behind on one partition while thirty-one sit idle. Three triggers, one surface programming model ([ServiceBusTrigger], [EventGridTrigger], [EventHubTrigger] are all just attributes), and three completely different machines underneath. The trigger you choose decides whether delivery is pull or push, whether you get retries and a dead-letter queue or best-effort, how the host scales, and what “I processed this message” even means.

This article makes those differences concrete. We treat the three triggers as three distinct delivery contracts: Service Bus (a broker you pull with peek-lock, retries and a dead-letter sub-queue — for work that must not be lost), Event Grid (a push-based router that validates your endpoint with a handshake and fans reactive notifications out to many subscribers), and Event Hubs (a partitioned stream you read with checkpoints, for high-throughput telemetry where you replay by offset). For each you will learn the binding, the host.json knobs that matter, how the scale controller adds instances, how to use a managed identity instead of a connection string, and the production failure modes — with the exact az command or portal blade to confirm each, plus a Bicep version.

By the end you will stop reaching for whichever trigger you used last and start choosing on the delivery semantics the workload needs. When an event “didn’t arrive,” you will know whether to peek the dead-letter queue, re-run the Event Grid validation handshake, or check a partition’s checkpoint — and you will know it in minutes, because you will understand which machine is actually running under the attribute.

What problem this solves

The Functions programming model is deliberately uniform: decorate a method with a trigger attribute, declare input/output bindings, and the runtime handles connection, deserialization, retries and scaling. That uniformity is the appeal — and the trap. Because all three triggers look identical in code, teams pick one by habit and inherit delivery semantics they never chose. A team needing guaranteed, retryable command processing reaches for Event Grid because “it’s events,” loses messages on transient failures, and has no DLQ to recover them; another ingesting telemetry uses Service Bus and hits the per-message overhead wall at a few thousand messages a second.

What breaks is rarely a crash — it is silent wrong behaviour: messages vanish because a best-effort trigger dropped them on a transient error; the same message is processed twice because the code assumed exactly-once when the platform promises only at-least-once; throughput plateaus because per-message acknowledgement was never meant for a firehose; a subscription never delivers because the handshake failed and nobody looked at provisioningState. None of these throw an exception you can grep for — they surface as missing data, duplicate side effects, or latency, the hardest failures to diagnose after the fact.

Who hits this: every team building event-driven systems on Azure. It bites hardest on developers moving to a mixed broker estate, on engineers who treat a stream like a queue (or vice versa), and on anyone who enabled a trigger, saw it work once with a test message, and shipped without testing a transient failure, a poison message, or load. The fix is not more code; it is choosing the trigger whose delivery contract matches the workload.

To frame the field before the deep dive — each trigger’s identity, the workload it’s built for, and its most common mistake:

Trigger Delivery model Built for Acknowledgement Most common mistake
Service Bus Pull (broker, peek-lock) Commands, workflows, ordered/guaranteed work Complete/abandon per message; DLQ on failure Treating it as fire-and-forget; ignoring the DLQ
Event Grid Push (HTTP webhook) Reactive notifications, fan-out to many handlers HTTP 2xx = delivered; retries then dead-letter to blob Validation handshake fails; subscription never delivers
Event Hubs Stream (partitioned, checkpointed) High-throughput telemetry, event streaming Checkpoint by offset, per batch — not per message Using it like a queue; one hot partition; double-processing

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand the Functions basics: a function app is the deployment and scale unit; inside it each function has exactly one trigger plus optional input/output bindings; and the app runs on a hosting plan (Consumption, Flex Consumption, Premium, or Dedicated). You should be comfortable running az in Cloud Shell and know what a Service Bus queue, an Event Grid topic and an Event Hub are. Familiarity with managed identities and Azure RBAC helps, because identity-based connections are the modern default.

If the trigger-and-binding mental model is new, start with Azure Functions Triggers and Bindings for Beginners and Wiring Azure Functions to Storage Queues and Cosmos DB. This article sits one level up — it assumes you can wire a simple trigger — and pairs with the broker deep-dives Service Bus Queues vs Topics and Event Hubs Fundamentals, which explain the brokers themselves; here we focus on how a Function consumes them.

When something breaks, the owning layer differs by symptom: the producer (wrong schema, no events, key skew), the broker (throttling, DLQ full, validation pending, partition lag), the connection (a missing RBAC role → the trigger never fires), the host/scale controller (under-scaling, double-processing), the handler (poison messages, duplicate side effects), or the output binding/downstream (partial writes, throttling).

Core concepts

Five mental models make every later decision obvious.

The trigger is a listener, not just an attribute. When the host starts, each trigger spins up a listener that connects to its source and either pulls work (Service Bus, Event Hubs) or registers an HTTP endpoint to be pushed to (Event Grid). The attribute ([ServiceBusTrigger("orders")]) is metadata the runtime reads to build that listener via an extension (the Microsoft.Azure.*.Extensions.* package — each trigger has its own, installed in the project). The listener is the real machine; the attribute describes it.

Pull, push and stream are three different contracts. A pull trigger (Service Bus, Event Hubs) has the host fetch on its own schedule, so the host controls concurrency, prefetch and back-pressure. A push trigger (Event Grid) has the source deliver to an HTTP endpoint, so the source controls timing and retries, and your endpoint must survive a validation handshake first. A stream (Event Hubs) is a pull of an append-only log read by offset with checkpoints — you can replay, and “done” means “I checkpointed past here,” not “I removed this message.”

At-least-once is the universal promise; exactly-once is your job. None of these triggers gives exactly-once delivery to your handler — Service Bus and Event Grid redeliver on failure, Event Hubs reprocesses from the last checkpoint on restart. Therefore idempotency is not optional: your handler must tolerate seeing the same message twice. Duplicate-detection windows and Durable Functions help, but the baseline contract is at-least-once, full stop.

Dead-lettering means three different things. Service Bus has a first-class DLQ — after MaxDeliveryCount failures (default 10) the broker moves the message to a $DeadLetterQueue sub-queue you inspect and replay. Event Grid retries on a schedule and, if you set a dead-letter destination (a blob container), drops undeliverable events there after the retry window. Event Hubs has no built-in poison handling — a crashing record is re-read from the checkpoint forever unless your code routes it out. Knowing which model you’re in is the difference between a recoverable failure and an infinite loop.

Scaling is signal-driven and trigger-specific. A scale controller watches each trigger’s backlog and adds or removes instances: queue/subscription depth for Service Bus, unprocessed events per partition for Event Hubs (hard-capped at one instance per partition), and HTTP concurrency for Event Grid. The newer target-based scaling model scales in bigger steps. The ceiling differs per trigger — you cannot get more Event Hubs concurrency than you have partitions.

The three triggers compared

Before the per-trigger deep dives, the master comparison — the one table to bookmark, because it drives every choice that follows:

Dimension Service Bus Event Grid Event Hubs
Delivery model Pull (peek-lock) Push (HTTP webhook) Pull (partitioned stream)
Built for Commands, workflows, transactions Reactive notifications, fan-out High-throughput telemetry/streaming
Ordering FIFO per session (with sessions) None guaranteed Per partition
Delivery guarantee At-least-once At-least-once At-least-once (from checkpoint)
Retries Broker redelivers up to MaxDeliveryCount Exponential, up to 24h window Re-read from last checkpoint
Poison handling Dead-letter sub-queue (built-in) Dead-letter to blob (opt-in) None built-in — your code must route
Acknowledgement Complete/abandon per message HTTP 2xx response Checkpoint by offset (per batch)
Replay No (consumed = gone) No Yes (rewind to any offset in retention)
Scaling unit Queue/subscription depth HTTP concurrency Partition count (1 instance/partition max)
Throughput ceiling High (per-message overhead) Very high (event router) Highest (millions/sec with enough TUs)
Message size 256 KB (Std) / 100 MB (Premium) 1 MB per event 1 MB per event
Typical latency Low (ms) Low (sub-second) Low (ms, batched)

In one sentence each: choose Service Bus when losing or duplicating a message has business consequences and you need retries plus a recovery queue; Event Grid to react to something and fan it out to independent handlers; Event Hubs when volume is high, order-within-a-shard matters, and you may need to replay. A common mistake conflates Event Grid and Event Hubs because both say “Event” — one is a discrete-notification router, the other a streaming pipe. They are not alternatives.

If the workload is… The signal is… Choose Because
“Process this order exactly, never lose it” A command that must complete Service Bus Peek-lock + DLQ + retries
“A blob was created, do something” A discrete state change Event Grid (system topic) Native source, push, fan-out
“Millions of IoT readings per minute” A high-volume telemetry firehose Event Hubs Partitioned stream, replay
“Notify 5 unrelated services of an event” One event, many independent reactions Event Grid One publish, N subscriptions
“Strict FIFO per customer” Ordered work keyed by entity Service Bus (sessions) Session = ordered, single-consumer
“Replay the last 3 days for reprocessing” Need to rewind history Event Hubs Offset-based replay within retention

The Service Bus trigger in depth

The Service Bus trigger is a pull listener that fetches messages from a queue or topic subscription using peek-lock. The host receives a message, your function runs, and on success the runtime completes it (removes it); on an unhandled exception it abandons it (the lock releases and the broker redelivers). After MaxDeliveryCount failed deliveries — default 10 — the broker moves the message to the dead-letter sub-queue. Use this trigger whenever a message represents work that must not be silently lost.

The binding in the isolated (.NET 8+) model:

[Function("ProcessOrder")]
public async Task Run(
    [ServiceBusTrigger("orders", Connection = "SbConnection")] ServiceBusReceivedMessage message,
    FunctionContext context)
{
    var logger = context.GetLogger("ProcessOrder");
    logger.LogInformation("Order {id}, delivery #{n}", message.MessageId, message.DeliveryCount);
    // process idempotently; throw to abandon → retry → eventually DLQ
}

The Connection value names an app setting (a connection string or the prefix of an identity-based connection, below). For a topic subscription you add TopicName/SubscriptionName.

The host.json settings that actually matter

The Service Bus extension’s host.json block controls concurrency and prefetch — get these wrong and you either under-utilise the app or overwhelm a downstream:

Setting (extensions.serviceBus) What it does Default When to change
maxConcurrentCalls Max messages processed in parallel per instance (non-session) 16 Lower to protect a fragile downstream; raise for CPU-light work
maxConcurrentSessions Max sessions processed in parallel (session-enabled) 8 Tune session throughput
prefetchCount Messages fetched ahead into memory 0 Raise to cut round-trips on high throughput; costs lock time
autoCompleteMessages Auto-complete on success / abandon on exception true Set false only if you call Complete/Abandon yourself
maxAutoLockRenewalDuration How long the runtime keeps renewing the lock 5 min Raise for long-running handlers; cap by your processing time
messageHandlerOptions.maxConcurrentCalls (legacy host) Same as above on older runtime 16 Legacy in-process apps

For a guaranteed-delivery workload talking to a rate-limited API, a sane host.json caps maxConcurrentCalls to ~8, sets prefetchCount to ~20, leaves autoCompleteMessages true, and raises maxAutoLockRenewalDuration to match the longest handler.

Poison messages and the dead-letter queue

The behaviour to internalise: a message that always throws is retried MaxDeliveryCount times, then dead-lettered — not lost, not looped forever. That is the feature. Make the handler idempotent (so a transient failure plus a retry doesn’t double-process) and have a plan for the DLQ (<queue>/$DeadLetterQueue) — trigger a second function on it to alert and replay.

Behaviour How to control it Where
Retries before dead-letter MaxDeliveryCount (1–2147483647, default 10) Queue/subscription property
Lock duration per delivery LockDuration (max 5 min) Queue/subscription property
Auto dead-letter on expiry DeadLetteringOnMessageExpiration Queue/subscription property
Inspect dead-lettered messages Peek $DeadLetterQueue Service Bus Explorer / SDK
Recover / replay Read DLQ, resubmit to main queue Custom function or tool
# Messages in the queue vs its dead-letter sub-queue
az servicebus queue show --namespace-name sb-shop-prod --resource-group rg-shop-prod \
  --name orders \
  --query "{active:countDetails.activeMessageCount, dead:countDetails.deadLetterMessageCount}" -o table

A non-zero, climbing dead count means a permanent failure is dead-lettering — go read those messages rather than raising MaxDeliveryCount.

Sessions and ordering

By default Service Bus delivers to many concurrent handlers with no ordering — fast, but unordered. For FIFO per entity (all events for customer 42 in order), enable sessions on the queue and set IsSessionsEnabled = true on the trigger; the broker hands each session to one consumer at a time, preserving order within it at the cost of cross-session parallelism. (For near-exactly-once, a duplicate-detection window on the queue suppresses repeats at roughly a 20% throughput cost.)

The Event Grid trigger in depth

Event Grid is a push model, and that one fact explains everything strange about it. Instead of your function pulling, Event Grid POSTs events to an HTTP endpoint the function exposes. Before delivering any real event, Event Grid runs a validation handshake: it sends a SubscriptionValidationEvent and expects your endpoint to echo back the validationCode. The Functions Event Grid extension handles this automatically — which is why you use the extension’s trigger rather than rolling your own, and why a failed handshake leaves the subscription stuck with no events ever arriving.

The binding (isolated model):

[Function("OnBlobCreated")]
public void Run([EventGridTrigger] CloudEvent cloudEvent, FunctionContext context)
{
    var logger = context.GetLogger("OnBlobCreated");
    logger.LogInformation("Type {t} subject {s}", cloudEvent.Type, cloudEvent.Subject);
    // return normally = 2xx = delivered; throw = non-2xx = retried
}

Note there is no Connection — Event Grid pushes to you, so auth is on the subscription side (a system key in the webhook URL, or the native azurefunction binding via the portal). Your handler’s return is the acknowledgement: return normally and delivery is recorded; throw and Event Grid retries.

The validation handshake — the #1 Event Grid gotcha

Because the handshake is invisible when it works and fatal when it doesn’t, here is the exact flow and how to confirm it:

Step What happens If it fails
1. Create subscription Event Grid needs to verify you own the endpoint
2. Validation event sent EG POSTs SubscriptionValidationEvent with a validationCode
3. Endpoint echoes code Extension responds with { "validationResponse": "<code>" } No echo → provisioningState stays not-Succeeded
4. Subscription active EG begins delivering real events If stuck, no events ever flow

Confirm the subscription’s state — the single command that tells you whether the handshake worked:

# provisioningState 'Succeeded' = handshake passed and it will deliver
az eventgrid event-subscription show \
  --name fn-blob-sub \
  --source-resource-id $(az storage account show -n stshopdata -g rg-shop-prod --query id -o tsv) \
  --query "{state:provisioningState, endpointType:destination.endpointType}" -o table

If provisioningState is anything other than Succeeded, the endpoint didn’t echo the validation code (extension not installed, wrong route, app cold/unreachable at creation, or the wrong system key). Fix the endpoint and re-create the subscription; Event Grid retries the handshake.

Retry, dead-lettering and event delivery

Event Grid retries failed deliveries on a back-off schedule for up to 24 hours, then drops the event — unless you set a dead-letter destination (a blob container), where undeliverable events land for inspection. This is opt-in and easy to forget, which is how reactive events silently disappear.

Setting What it controls Default Notes
Max delivery attempts Retry attempts before giving up 30 Configurable on the subscription
Event time-to-live Retry window 1440 min (24h) After this, dropped or dead-lettered
Dead-letter destination Blob container for undeliverable events none (off) Opt-in — set it or lose failed events
Advanced filters Deliver only matching events none Filter by event type, subject prefix/suffix, data fields
Delivery schema CloudEvents 1.0 or Event Grid schema EventGrid CloudEvents recommended for portability

Set the dead-letter destination with az eventgrid event-subscription update ... --deadletter-endpoint <storage-id>/blobServices/default/containers/<name> — undeliverable events then land in that blob container instead of being dropped.

System topics vs custom topics

Two ways events reach the trigger. A system topic is built into an Azure resource — a storage account emits Microsoft.Storage.BlobCreated, a resource group emits Microsoft.Resources.ResourceWriteSuccess — so you subscribe a function to Azure’s own events with no publisher code, using the resource-defined Event Grid schema. A custom topic is one you create and your application publishes domain events to (CloudEvents recommended). For reacting to Azure infrastructure, system topics are the free, native path; see Event Grid System Topics Explained for the source list and How to Build an Event Grid Custom Topic for publishing your own.

The Event Hubs trigger in depth

Event Hubs is a partitioned stream, and the trigger reads it like a log file: by offset, in batches, recording a checkpoint to mark how far it got. It is built for volume — telemetry, clickstreams, IoT — where Service Bus’ per-message acknowledgement would be far too expensive. Three defining constraints: ordering is per partition only, concurrency is capped at one instance per partition, and there is no built-in poison handling — a crashing record is re-read from the checkpoint forever unless your code routes it out.

The binding (isolated model, batched — the efficient default):

[Function("IngestTelemetry")]
public async Task Run(
    [EventHubTrigger("telemetry", Connection = "EhConnection", ConsumerGroup = "fn")]
    EventData[] events,
    FunctionContext context)
{
    var logger = context.GetLogger("IngestTelemetry");
    foreach (var e in events)
    {
        // checkpoints AFTER the batch returns; throwing aborts it → no checkpoint → re-read
    }
}

Always give the function its own consumer group (ConsumerGroup = "fn") so its checkpoint state is isolated; sharing $Default with other consumers causes contention and confusing replays.

Checkpoints, offsets and exactly-once myths

A checkpoint is “I have processed up to offset N on partition P.” On restart (deploy, scale, crash), the trigger resumes from the last checkpoint — which is why Event Hubs is at-least-once: if the host processed a batch but crashed before checkpointing, that batch is read again. There is no exactly-once to your handler. The checkpoint store is the app’s AzureWebJobsStorage account (a blob per consumer group/partition); if it’s unreachable, checkpointing fails and you reprocess.

Concept Meaning Consequence
Offset Position of a record within a partition Resume point; monotonic per partition
Checkpoint Saved offset in AzureWebJobsStorage blob Where reprocessing starts after restart
At-least-once A batch may be re-read after a crash Handler MUST be idempotent
Retention How long records stay (1–7 days Std; up to 90 Premium) The replay window
$Default vs custom group Shared vs isolated reader view Use a dedicated group per app

Batching and the host.json knobs

Event Hubs throughput lives or dies on batching — these settings govern how many events you get per invocation and how often you checkpoint:

Setting (extensions.eventHubs) What it does Default When to change
maxEventBatchSize Max events per function invocation 100 (10 on older versions) Raise for throughput; lower to bound memory/latency
batchCheckpointFrequency Checkpoint every N batches 1 Raise to checkpoint less often (faster, more replay on crash)
prefetchCount Events fetched ahead 300 Tune for very high throughput
maxBatchSize (legacy name) Older alias of batch size Legacy runtime
initialOffsetOptions.type Where a new consumer starts fromStart fromEnd to skip backlog on first run

A high-throughput host.json might set maxEventBatchSize to 256, batchCheckpointFrequency to 5 and prefetchCount to 500. That subtle trade-off: raising batchCheckpointFrequency checkpoints less often (faster, fewer storage writes) but a crash re-reads more — throughput against replay volume. Fine for idempotent handlers; keep it low for expensive side effects.

Partitions, scaling and the one-instance-per-partition ceiling

The constraint people miss: the scale controller never runs more instances of an Event Hubs trigger than the hub has partitions. A 4-partition hub caps at 4 concurrent instances, no matter how high maxScaleOutCount or how deep the backlog. Partition count is set at creation (1–32 on Standard; 100+ on Premium/Dedicated) and is the parallelism dial. A hot partition — a low-cardinality partitionKey sending most events to one shard — pins the load on one instance while the others idle, the classic “Event Hubs is slow” report.

Symptom Cause Confirm Fix
Consumer falls behind globally Not enough partitions / TUs IncomingMessages >> OutgoingMessages Add throughput units; more partitions (new hub)
One partition lags, rest idle Hot partition (skewed key) Per-partition metrics; one partition’s lag high Use a higher-cardinality partitionKey (or null = round-robin)
Scales to N then stops Hit partition-count ceiling Instance count == partition count More partitions (requires a new hub on Standard)
Reprocesses on every deploy Checkpoint store unreachable Host error; storage firewall blocks app Open AzureWebJobsStorage firewall to the app

For the full partitioning model — how keys map to shards, consumer groups, and offsets — see Event Hubs Fundamentals.

Identity-based connections (no connection strings)

All three triggers historically used a connection string — a secret granting full data-plane access that you must store, rotate and protect. The modern default is an identity-based connection: the app authenticates via its managed identity, and you grant that identity a least-privilege RBAC data role on the namespace — no secret, no rotation, scoped to what the trigger needs. (For system-assigned vs user-assigned, see Managed Identities Demystified.)

Instead of a full connection string, provide the fully-qualified namespace under a connection prefix:

# Set the FQDN (not a connection string) under the connection prefix
az functionapp config appsettings set -n fn-shop-prod -g rg-shop-prod \
  --settings "SbConnection__fullyQualifiedNamespace=sb-shop-prod.servicebus.windows.net"
# Event Hubs equivalent: EhConnection__fullyQualifiedNamespace=eh-shop-prod.servicebus.windows.net

The binding’s Connection = "SbConnection" resolves to that setting and the runtime acquires a token via the app’s identity. Then grant the role — the step whose absence makes a trigger silently never fire:

Service Least-privilege role to RECEIVE Role to also SEND (output) Scope
Service Bus Azure Service Bus Data Receiver Azure Service Bus Data Sender Namespace (or queue/topic)
Event Hubs Azure Event Hubs Data Receiver Azure Event Hubs Data Sender Namespace (or hub)
Event Grid (push model — EG authenticates to you) EventGrid Data Sender (to publish) Topic
AzureWebJobsStorage Storage Blob Data Owner (checkpoints/leases) Storage account
# Enable the identity and grant it receive on the namespace
az functionapp identity assign -n fn-shop-prod -g rg-shop-prod
PRINCIPAL=$(az functionapp identity show -n fn-shop-prod -g rg-shop-prod --query principalId -o tsv)
SB_ID=$(az servicebus namespace show -n sb-shop-prod -g rg-shop-prod --query id -o tsv)
az role assignment create --assignee "$PRINCIPAL" \
  --role "Azure Service Bus Data Receiver" --scope "$SB_ID"

(The Bicep equivalent appears in the lab’s template below.) The failure signature when this role is missing is cruel: the function deploys and the host starts, no exception is thrown in your code, yet the trigger never fires — because the listener can’t authenticate to the data plane. The tell is an Unauthorized in the host logs, not your application logs.

Architecture at a glance

The diagram below walks the event-driven path left to right. On the left, producers emit work: an Azure resource (a storage account) raising native events, and your own apps sending messages or publishing events. These flow into the middle band — the three brokers, each with its own delivery contract: Service Bus under peek-lock with a dead-letter sub-queue, Event Grid pushing validated webhooks, and Event Hubs buffering a partitioned stream. The single Function App hosts all three triggers; its managed identity (granted the Data Receiver role) authenticates to the brokers, and its AzureWebJobsStorage account holds the Event Hubs checkpoints and the singleton leases that stop two instances grabbing the same work. From there the function writes through output bindings to Cosmos DB and other systems, while emitting traces to Application Insights on the right.

Follow the numbered badges to where delivery actually breaks: a poison message dead-lettering on Service Bus, an Event Grid webhook validation that never completed, an Event Hubs hot partition falling behind, the authorization gap when the identity lacks its role, and a checkpoint/lease store that’s unreachable. The legend narrates each as symptom, check and fix — the same diagnostic map the troubleshooting section expands.

Left-to-right event-driven architecture showing producers (storage account, apps) publishing into three brokers (Service Bus with peek-lock and DLQ, Event Grid push webhook, Event Hubs with 32 partitions), all feeding one Function App whose triggers, managed identity and AzureWebJobsStorage checkpoint store process the events, then write to Cosmos DB and SQL via output bindings and emit telemetry to Application Insights; five numbered badges mark the poison-message DLQ, the Event Grid validation handshake, an Event Hubs hot partition, the identity authorization failure, and the lease/checkpoint store outage

Real-world scenario

Meridian Retail rebuilt its order pipeline around Azure Functions with three integrations: a Service Bus queue driving a ProcessOrder function (charge payment, decrement inventory — must not be lost or double-charged), an Event Grid system topic triggering GenerateThumbnails when a product photo lands in blob storage, and Event Hubs streaming page views into a RollupAnalytics function. One team, three triggers, three delivery models.

It worked in staging and broke three ways in the first production month. The order pipeline started dead-lettering — the DLQ count climbed and orders weren’t confirming. The on-call engineer’s instinct was to raise MaxDeliveryCount, which only wasted more retries per doomed message. Peeking the dead-letter sub-queue (deadLetterMessageCount at 1,400 and rising) revealed the cause: a downstream inventory API had renamed a field, every order threw the same deserialization error, and after 10 abandons the broker dead-lettered them. A code fix plus a DLQ-replay function recovered all 1,400 stranded orders — none lost, precisely because Service Bus dead-lettered rather than dropped. The lesson: a filling DLQ is a symptom to read, not a dial to turn.

The thumbnail function simply never ran — no errors, no invocations. The Event Grid subscription existed but was created while the app was scaled to zero and slow to wake, so the validation handshake failed and provisioningState was stuck. az eventgrid event-subscription show confirmed a non-Succeeded state; re-creating it once the app was warm fixed it instantly, and Always-Ready instances kept the endpoint reachable thereafter. The lesson: an Event Grid subscription that “doesn’t deliver” is almost always a failed handshake — check provisioningState first.

The analytics function fell hours behind during a flash sale. IncomingMessages far exceeded OutgoingMessages — but only on one of four partitions. The producer used customerRegion as the partition key and 70% of traffic came from one region, pinning that partition to a single instance while the one-instance-per-partition ceiling stopped the scale controller from helping. Switching the key to the higher-cardinality sessionId spread the load, and a new 16-partition hub lifted the ceiling. The lesson: Event Hubs concurrency is capped by partition count, and a low-cardinality key wastes the partitions you have. Three triggers, three failure modes, each diagnosed in minutes once the team read the trigger as a delivery contract, not just a connection.

Advantages and disadvantages

Each trigger is the right answer for its workload and the wrong answer outside it:

Trigger Advantages Disadvantages
Service Bus Guaranteed at-least-once; built-in retries + DLQ; sessions for FIFO; transactions; rich filtering Per-message overhead caps throughput; 256 KB messages (Std); more cost per message; no replay
Event Grid Native Azure-event sources (free system topics); massive fan-out; push = no polling cost; sub-second Validation handshake friction; dead-lettering is opt-in; no ordering; not for high-volume streaming
Event Hubs Highest throughput; replay within retention; per-partition ordering; cheap per event at scale No built-in poison handling; one-instance-per-partition ceiling; checkpoint complexity; hot-partition risk

When each matters: Service Bus earns its overhead the moment a lost or duplicated message has business cost — payments, inventory, anything transactional. Event Grid shines for reactive fan-out, and its system topics turn Azure’s own state changes into triggers for free. Event Hubs is the only sane choice once volume crosses thousands of events per second or you need to replay. Choosing wrong is rarely catastrophic on day one — it’s the month-two surprise the trade-off table prevents.

Hands-on lab

You will stand up all three triggers against one function app, wire each to its broker with an identity-based connection, fire a test event into each, confirm it ran, and tear everything down — the centerpiece of this article. It is free-tier-friendly: Service Bus Basic, a free Event Grid system topic, an Event Hubs Basic namespace and a Consumption function app cost a few rupees for the hour and nothing once deleted. We give the portal path for orientation, the az CLI as the authoritative copy-pasteable version, and a Bicep template for the infrastructure. Run the CLI in Cloud Shell (Bash).

Part A — Prerequisites and variables

Step 1 — Set variables and a resource group. Everything is named off these for one-command cleanup.

RG=rg-fn-triggers-lab
LOC=centralindia
SB=sb-fnlab-$RANDOM           # Service Bus namespace (globally unique)
EH=eh-fnlab-$RANDOM           # Event Hubs namespace (globally unique)
ST=stfnlab$RANDOM             # storage acct: lowercase, no dashes, ≤24 chars
FN=fn-triggers-$RANDOM        # function app (globally unique)
az group create -n $RG -l $LOC -o table

Expected: a resource group row with provisioningState: Succeeded.

Step 2 — Confirm the CLI extension and provider. The Event Grid commands need the eventgrid extension and the Microsoft.EventGrid provider registered:

az extension add --name eventgrid --upgrade 2>/dev/null
az provider register --namespace Microsoft.EventGrid --wait

Expected: the provider’s registrationState is Registered (check with az provider show --namespace Microsoft.EventGrid --query registrationState -o tsv).

Part B — Create the brokers and the function app (CLI)

Step 3 — Service Bus namespace + queue (with a real dead-letter setting).

az servicebus namespace create -n $SB -g $RG --sku Basic -o table
az servicebus queue create --namespace-name $SB -g $RG --name orders \
  --max-delivery-count 5 -o table

Expected: a queue row; note maxDeliveryCount: 5 — after 5 failed deliveries a message dead-letters. (Basic SKU supports queues; topics need Standard.)

Step 4 — Event Hubs namespace + hub + dedicated consumer group.

az eventhubs namespace create -n $EH -g $RG --sku Basic -o table
az eventhubs eventhub create --namespace-name $EH -g $RG --name telemetry \
  --partition-count 2 --message-retention 1 -o table
az eventhubs eventhub consumer-group create --namespace-name $EH -g $RG \
  --eventhub-name telemetry --name fn -o table

Expected: an event hub with partitionCount: 2 and a consumer group fn. Event Hubs Basic supports only the $Default consumer group, so the consumer-group create line needs --sku Standard on the namespace (use it if that command errors).

Step 5 — Storage account and function app (Consumption, .NET isolated). The storage backs AzureWebJobsStorage (checkpoints + leases).

az storage account create -n $ST -g $RG -l $LOC --sku Standard_LRS -o table
az functionapp create -n $FN -g $RG \
  --storage-account $ST --consumption-plan-location $LOC \
  --runtime dotnet-isolated --functions-version 4 --os-type Linux -o table

Expected: a function app row, state: Running, kind containing functionapp,linux.

Step 6 — Enable the managed identity and grant the data-plane roles — the step that makes the triggers fire without secrets.

az functionapp identity assign -n $FN -g $RG -o table
PRINCIPAL=$(az functionapp identity show -n $FN -g $RG --query principalId -o tsv)

# Receive roles scoped to each namespace
az role assignment create --assignee "$PRINCIPAL" --role "Azure Service Bus Data Receiver" \
  --scope $(az servicebus namespace show -n $SB -g $RG --query id -o tsv)
az role assignment create --assignee "$PRINCIPAL" --role "Azure Event Hubs Data Receiver" \
  --scope $(az eventhubs namespace show -n $EH -g $RG --query id -o tsv)

# Verify the assignments landed
az role assignment list --assignee "$PRINCIPAL" --query "[].roleDefinitionName" -o tsv

Expected output: the two role names printed. If they’re missing, the triggers will deploy but never fire — re-run the create commands.

Step 7 — Wire the identity-based connections as app settings (the binding Connection prefixes resolve to these):

az functionapp config appsettings set -n $FN -g $RG --settings \
  "SbConnection__fullyQualifiedNamespace=$SB.servicebus.windows.net" \
  "EhConnection__fullyQualifiedNamespace=$EH.servicebus.windows.net" -o none
echo "App settings set."

Expected: App settings set. (Event Grid needs no connection — it pushes to the function.)

Part C — The function code (three triggers, one app)

Step 8 — The three handlers (isolated .NET 8). Create Functions.cs and deploy with your normal pipeline (func azure functionapp publish $FN).

public class Triggers
{
    [Function("ProcessOrder")]
    public void ProcessOrder(
        [ServiceBusTrigger("orders", Connection = "SbConnection")] string body,
        FunctionContext ctx)
        => ctx.GetLogger("ProcessOrder").LogInformation("Order: {b}", body);

    [Function("OnImageUploaded")]
    public void OnImageUploaded([EventGridTrigger] CloudEvent e, FunctionContext ctx)
        => ctx.GetLogger("OnImageUploaded").LogInformation("EG {t}: {s}", e.Type, e.Subject);

    [Function("RollupTelemetry")]
    public void RollupTelemetry(
        [EventHubTrigger("telemetry", Connection = "EhConnection", ConsumerGroup = "fn")] string[] batch,
        FunctionContext ctx)
        => ctx.GetLogger("RollupTelemetry").LogInformation("EH batch of {n}", batch.Length);
}

The project must reference the three extensions (Microsoft.Azure.Functions.Worker.Extensions.ServiceBus, ...EventGrid, ...EventHubs) and a host.json with the settings from the deep-dive sections above.

Step 9 — Create the Event Grid subscription (the handshake). This points a storage system topic at the OnImageUploaded function; the extension echoes the validation code automatically.

# Subscribe the function to the storage account's BlobCreated events
SRC=$(az storage account show -n $ST -g $RG --query id -o tsv)
FNID=$(az functionapp show -n $FN -g $RG --query id -o tsv)
az eventgrid event-subscription create --name img-sub \
  --source-resource-id $SRC \
  --endpoint-type azurefunction \
  --endpoint "$FNID/functions/OnImageUploaded" \
  --included-event-types Microsoft.Storage.BlobCreated -o table

Expected: provisioningState: Succeeded. If it is anything else, the handshake failed (function not deployed yet, or app cold) — deploy the code first, then re-run.

Part D — Fire a test event into each trigger and validate

Step 10 — Service Bus: send a message, confirm it processed (or dead-lettered).

# Send an order message (or use Service Bus Explorer in the portal)
az servicebus queue message send 2>/dev/null \
  || echo "Use Service Bus Explorer (portal) if 'message send' is unavailable."
# active should drain to 0 as the function processes; dead should stay 0
az servicebus queue show --namespace-name $SB -g $RG --name orders \
  --query "{active:countDetails.activeMessageCount, dead:countDetails.deadLetterMessageCount}" -o table

Expected: after the function runs, active: 0, dead: 0. If dead climbs, your handler is throwing — read the DLQ.

Step 11 — Event Grid: upload a blob to trigger the system topic.

echo "hello" > /tmp/test.jpg
az storage blob upload --account-name $ST --auth-mode login \
  -c '$web' -f /tmp/test.jpg -n test.jpg 2>/dev/null \
  || az storage blob upload --account-name $ST --auth-mode login -c images -f /tmp/test.jpg -n test.jpg

Expected: the upload succeeds and, within a second or two, the OnImageUploaded function logs a Microsoft.Storage.BlobCreated event. (Create the container first if needed: az storage container create -n images --account-name $ST --auth-mode login.)

Step 12 — Event Hubs: send an event, confirm a batch invocation.

# Send an event (or use the portal's Data Explorer → Send events)
az eventhubs eventhub send 2>/dev/null \
  || echo "Use the Event Hubs portal 'Data Explorer → Send events'."

Expected: the RollupTelemetry function logs EH batch of N (the first run on a fresh checkpoint may read from the start of the stream).

Step 13 — Confirm all three in Application Insights. Functions auto-wires App Insights; query the invocations:

// Last 30 min: which functions ran and their success rate
requests
| where timestamp > ago(30m)
| summarize invocations=count(), failures=countif(success==false) by operation_Name
| order by invocations desc

Expected: rows for ProcessOrder, OnImageUploaded and RollupTelemetry, each with invocations >= 1 and failures == 0.

Validation checklist. You created three brokers and one function app whose managed identity holds the Data Receiver roles, wired identity-based connections (no secrets), got an Event Grid subscription past its handshake, and fired one event into each trigger to see all three invoke. The steps mapped to what each proves:

Step What you did What it proves
6 Grant Data Receiver roles Identity-based triggers fire only with the right RBAC
9 Create EG subscription The validation handshake must reach Succeeded
10 Send + check DLQ Service Bus completes on success, dead-letters on failure
11 Upload a blob A system topic turns an Azure event into a trigger
12 Send to the hub The stream trigger reads in batches per partition
13 App Insights query One pane confirms all three delivery models worked

Part E — Bicep version of the infrastructure

For repeatable deploys, the infrastructure as Bicep (brokers, storage, function app, identity, role assignments) — deploy the function code on top with your pipeline:

@description('Location for all resources')
param location string = resourceGroup().location
param prefix string = 'fnlab'

resource sb 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
  name: '${prefix}-sb-${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Basic', tier: 'Basic' }
  resource orders 'queues' = { name: 'orders', properties: { maxDeliveryCount: 5 } }
}

resource eh 'Microsoft.EventHub/namespaces@2024-01-01' = {
  name: '${prefix}-eh-${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard', tier: 'Standard' }   // Standard so a custom consumer group is allowed
  resource hub 'eventhubs' = {
    name: 'telemetry'
    properties: { partitionCount: 2, messageRetentionInDays: 1 }
    resource cg 'consumergroups' = { name: 'fn' }
  }
}

resource st 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: toLower('${prefix}st${uniqueString(resourceGroup().id)}')
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: '${prefix}-plan'
  location: location
  sku: { name: 'Y1', tier: 'Dynamic' }   // Consumption
}

resource fn 'Microsoft.Web/sites@2023-12-01' = {
  name: '${prefix}-app-${uniqueString(resourceGroup().id)}'
  location: location
  kind: 'functionapp,linux'
  identity: { type: 'SystemAssigned' }
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      linuxFxVersion: 'DOTNET-ISOLATED|8.0'
      appSettings: [
        { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
        { name: 'AzureWebJobsStorage__accountName', value: st.name }
        { name: 'SbConnection__fullyQualifiedNamespace', value: '${sb.name}.servicebus.windows.net' }
        { name: 'EhConnection__fullyQualifiedNamespace', value: '${eh.name}.servicebus.windows.net' }
      ]
    }
  }
}

// Data-plane RBAC: RECEIVE on both namespaces
resource sbRx 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(fn.id, sb.id, 'sb-rx')
  scope: sb
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions',
      '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0') // Azure Service Bus Data Receiver
    principalId: fn.identity.principalId
    principalType: 'ServicePrincipal'
  }
}
resource ehRx 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(fn.id, eh.id, 'eh-rx')
  scope: eh
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions',
      'a638d3c7-ab3a-418d-83e6-5f17a39d4fde') // Azure Event Hubs Data Receiver
    principalId: fn.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

Deploy and validate it preflighted cleanly:

az deployment group create -g $RG --template-file main.bicep --parameters prefix=fnlab -o table

Expected: provisioningState: Succeeded for the deployment and each resource.

Part F — Teardown

Step 14 — Delete everything in one command. This stops all charges.

az group delete -n $RG --yes --no-wait

The Event Grid subscription is deleted with its source storage account inside the group, so one command zeroes all charges. A handful of test executions on Consumption is effectively free under the monthly grant.

Common mistakes & troubleshooting

The failure modes that actually bite, each as symptom → root cause → confirm → fix — first the scannable playbook, then detail on the non-obvious ones.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 Trigger never fires; no error in code Identity lacks the Data Receiver role Host logs show Unauthorized; az role assignment list --assignee <principalId> empty Grant Service Bus/Event Hubs Data Receiver on the namespace
2 Event Grid subscription delivers nothing Validation handshake failed az eventgrid event-subscription show --query provisioningStateSucceeded Deploy the function first; re-create the subscription when the app is warm
3 Service Bus DLQ filling up Handler throws every time (poison message) az servicebus queue show --query countDetails.deadLetterMessageCount > 0 and rising Fix the handler; replay the DLQ; don’t just raise MaxDeliveryCount
4 Same message processed twice Handler not idempotent; at-least-once redelivery App Insights shows duplicate operationIds / side effects Make the handler idempotent (dedupe key, upsert)
5 Event Hubs consumer falls behind, one partition Hot partition from a low-cardinality key Per-partition IncomingMessages skew; one partition’s lag high Use a higher-cardinality partitionKey; add partitions/TUs
6 Event Hubs scales to N then stops Hit the one-instance-per-partition ceiling Instance count == partition count More partitions (new hub on Standard)
7 Reprocesses the whole stream on deploy Checkpoint store (AzureWebJobsStorage) unreachable Host startup error; storage firewall blocks the app Open storage firewall to the app; verify the storage connection
8 Function times out on big batches maxEventBatchSize too high for the work Invocation duration near the plan timeout Lower maxEventBatchSize; raise plan timeout; process async
9 Throughput plateaus on Service Bus maxConcurrentCalls/prefetchCount too low CPU idle while backlog grows Raise concurrency/prefetch; protect downstream first
10 Messages lost on Event Grid failures No dead-letter destination configured Subscription has no deadLetterDestination Set a blob dead-letter endpoint on the subscription

Two of these reward a closer look.

#3 — Service Bus DLQ filling up. A climbing dead-letter count means a permanent failure is dead-lettering after MaxDeliveryCount retries. The wrong move is raising MaxDeliveryCount — that wastes more retries on doomed messages. The right move: peek the dead-letter sub-queue (Service Bus Explorer or the SDK), read the actual error, fix the cause, and replay the messages to the main queue. The DLQ existing is the feature; ignoring it is the bug.

#5/#6 — Event Hubs partition problems. A hot partition (low-cardinality key) pins load on one instance; the partition ceiling caps total concurrency at the partition count. Both report as “Event Hubs is slow” but have different fixes — spread the key, or add partitions for the ceiling. Confirm with per-partition metrics (Metrics → split by partition). On Standard you can’t add partitions to an existing hub, so plan partition count up front against peak parallelism.

Best practices

Security notes

The shift from connection strings to identity-based connections is the biggest security improvement for these triggers and should be your default. A connection string is a long-lived secret with broad data-plane rights; if it leaks (a commit, a log, an over-shared app setting) an attacker can read or write the broker. A managed identity has no secret to leak, and you grant the least-privilege role — Data Receiver to consume, Data Sender only if the function publishes — scoped to the specific namespace, queue or hub. Avoid the broad Owner/Contributor roles on data resources; they grant management-plane rights the trigger never needs.

Concern Risk Control
Secret leakage Connection string in code/logs grants full access Identity-based connection; no secret at all
Over-privileged access One role grants send+receive+manage Grant only Data Receiver (and Data Sender if publishing), scoped tight
Network exposure Broker reachable from the public internet Private endpoints on SB/EH; restrict to the app’s VNet
Event Grid endpoint abuse Anyone could POST fake events to the function System key in the webhook URL; validate event source/type
Data at rest Messages/events stored unencrypted Platform encrypts by default; customer-managed keys on Premium
Storage for checkpoints AzureWebJobsStorage holds operational state Restrict storage firewall to the app; identity-based storage connection

One topic-specific note: Event Grid’s push model inverts the auth direction — it authenticates to your function via the system key in the endpoint URL, so protect that key and validate the source/type of incoming events rather than trusting any POST. In a regulated environment, put brokers behind private endpoints and force the function’s egress through the VNet so neither a public endpoint nor a secret is exposed.

Cost & sizing

The bill comes from two places: the broker (per operation or per throughput unit) and the function executions (per invocation + GB-seconds on Consumption, or per instance-hour on Premium/Dedicated). The brokers price very differently, which feeds back into trigger choice at scale.

Driver Service Bus Event Grid Event Hubs
Billing unit Per operation (Basic/Std); per messaging unit (Premium) Per million operations (events) Per throughput unit (TU) + ingress; or Premium capacity
Cheapest tier Basic (queues only) Pay-per-event (very cheap) Basic (1 TU)
Scales cost with Message count + operations Event count Throughput units, not event count directly
Best cost profile Moderate volume, high value per message Spiky, reactive, fan-out Very high volume (cheap per event at scale)
Watch out for Per-operation cost at high volume Dead-letter storage; high event counts Idle TUs you pay for regardless of volume

Rough figures (vary by region; order-of-magnitude): Service Bus Basic is a few rupees per million operations; Event Grid is around ₹50–60 per million operations after a free grant; Event Hubs Basic is a small hourly charge per throughput unit — and an idle TU still costs. The function side on Consumption has a monthly free grant (1M executions + 400,000 GB-s) covering most low-volume integrations; for steady high-throughput streams, Premium or Flex Consumption often costs less and eliminates cold-start handshake failures. To right-size: cap Service Bus concurrency/prefetch (more isn’t free); match Event Hubs TUs to peak and don’t over-provision partitions; and use Event Grid advanced filters so a chatty source doesn’t bill for events you ignore.

Interview & exam questions

Q1. What’s the fundamental difference between the Service Bus and Event Grid triggers? Service Bus is pull — the function fetches messages with peek-lock, gets retries and a DLQ, and is built for guaranteed command/workflow processing. Event Grid is push — it POSTs events to an HTTP endpoint after a validation handshake and acknowledges via the HTTP response. One is a durable broker you drain; the other an event router that delivers to you. (AZ-204, AZ-305.)

Q2. An Event Grid subscription was created but no events are arriving. What do you check first? The subscription’s provisioningState. If it isn’t Succeeded, the validation handshake failed — the endpoint didn’t echo the validationCode. Deploy the function and re-create the subscription when the app is warm. (AZ-204.)

Q3. Why must Azure Functions message handlers be idempotent? Because all three triggers deliver at-least-once, not exactly-once: a crash after processing but before completing (Service Bus) or checkpointing (Event Hubs) redelivers the message. Idempotency — dedupe keys, upserts — prevents duplicate side effects. (AZ-204, AZ-305.)

Q4. How does the Service Bus dead-letter queue work, and what’s the wrong response to it filling up? After MaxDeliveryCount failures (default 10), the broker moves the message to the $DeadLetterQueue sub-queue — not lost, recoverable. The wrong response is raising MaxDeliveryCount (it wastes more retries on a doomed message); the right one is to peek the DLQ, fix the cause, and replay. (AZ-204.)

Q5. Why can’t an Event Hubs-triggered function scale beyond a certain point? The scale controller runs at most one instance per partition — a 4-partition hub caps at 4 concurrent instances regardless of backlog. To scale higher you need more partitions, which on Standard means a new hub. (AZ-204, AZ-305.)

Q6. What is a “hot partition” in Event Hubs and how do you fix it? A partition getting a disproportionate share of events because the partition key has low cardinality; it pins load on one instance while others idle. Fix with a higher-cardinality key (or null for round-robin) and, if needed, more partitions. (AZ-305.)

Q7. How do identity-based connections work, and what’s the most common failure? You set <Conn>__fullyQualifiedNamespace and the app authenticates via its managed identity. The common failure is forgetting the RBAC data role (Data Receiver) — the trigger then deploys but never fires, with an Unauthorized only in the host logs, not your code. (AZ-204.)

Q8. When would you choose Event Hubs over Service Bus? When volume is high (thousands+ events/sec), you need per-partition ordering and the ability to replay within the retention window, and per-message acknowledgement would be too costly. Service Bus is for lower-volume, high-value messages needing guaranteed delivery and a DLQ. (AZ-305.)

Q9. What does batchCheckpointFrequency trade off in the Event Hubs trigger? It checkpoints every N batches: higher values checkpoint less often (fewer storage writes, higher throughput) but a crash re-reads more events. Safe to raise for idempotent handlers, best kept low for expensive side effects. (AZ-204.)

Q10. What’s the difference between an Event Grid system topic and a custom topic? A system topic is built into an Azure resource that emits events natively — you subscribe with no publisher code. A custom topic is one you create and your application publishes to. System topics are the free, native path for reacting to Azure state changes. (AZ-204.)

Q11. How do you recover messages that Event Grid failed to deliver? Only if you configured a dead-letter destination (a blob container) on the subscription — undeliverable events land there after the retry window; without it, they’re dropped. Always set a dead-letter blob endpoint for events you can’t afford to lose. (AZ-204.)

Quick check

  1. Which trigger is push-based, and what must happen before it delivers any real event?
  2. After how many failed deliveries does a Service Bus message dead-letter by default, and where does it go?
  3. What hard ceiling caps the concurrency of an Event Hubs-triggered function?
  4. With an identity-based connection, what’s the symptom when the function’s identity lacks the Data Receiver role?
  5. Why must all three triggers’ handlers be idempotent?

Answers

  1. Event Grid is push-based; it must complete the validation handshake (echo the validationCode from the SubscriptionValidationEvent) and reach provisioningState: Succeeded before any real event is delivered.
  2. After MaxDeliveryCount failed deliveries (default 10), the broker moves the message to the $DeadLetterQueue sub-queue, where it can be inspected and replayed — not lost.
  3. One instance per partition. A hub with N partitions caps at N concurrent processing instances regardless of backlog or scale-out settings.
  4. The function deploys but the trigger never fires, with no exception in your code — only an Unauthorized in the host logs, because the listener can’t authenticate to the data plane.
  5. Because all three deliver at-least-once — a crash before completing (Service Bus) or checkpointing (Event Hubs), or an Event Grid retry, can deliver the same message twice; idempotency prevents duplicate side effects.

Glossary

Next steps

You can now choose and configure the right messaging trigger for any event-driven workload. Build outward:

Azure FunctionsService BusEvent GridEvent HubsTriggers and BindingsEvent-DrivenServerlessManaged Identity
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading