Azure Serverless

Wiring Azure Functions to Storage Queues and Cosmos DB: A Hands-On Input/Output Binding Tutorial

You have a queue full of work and a database that needs to absorb it. The naive version is a worker that opens a QueueClient, polls in a loop, deserialises each message, opens a CosmosClient, builds an item, handles the retry, acks the message, and prays the process never crashes mid-batch. That is fifty lines of plumbing you will write, get subtly wrong, and maintain forever. Azure Functions bindings delete almost all of it. A trigger binding wakes your function when a message lands on a Storage Queue and hands you the deserialised payload as a method parameter. An output binding takes whatever object you return and writes it to Cosmos DB — no client, no connection string in code, no retry loop. You write the one function in the middle that turns an order message into an order document, and the platform owns the rest.

This is a hands-on tutorial, not a tour. By the end you will have stood up the real thing twice — once by clicking through the Azure portal and once with az CLI — plus a Bicep template that provisions the whole stack reproducibly. You will see the exact function.json (or C# attribute / Python decorator) that declares the bindings, the app settings that wire them to your storage account and Cosmos container, and what the platform does when a message poisons. Every step has an expected output so you know immediately whether it worked, and the article ends with a teardown so the lab costs you nothing.

The mental shift bindings demand is small but real: you stop thinking “connect, then act” and start thinking “declare what flows in and what flows out, then write only the transform.” A queue message flows in through a trigger; a Cosmos document flows out through an output binding; the function body is pure logic. Get that contract right — the binding type, direction, the connection setting name, the queueName, the Cosmos databaseName/containerName — and you have a durable, autoscaling, pay-per-execution pipeline. Get one field wrong and the function silently never fires, which is exactly the failure mode this tutorial teaches you to diagnose.

What problem this solves

The pain bindings remove is integration boilerplate — the repetitive, error-prone glue between a message source and a data sink. Without bindings, every function that touches a queue and a database carries its own copy of: client construction, connection-string handling, trigger wiring, deserialisation, batching, retry-on-throttle, message acknowledgement, and dead-lettering. Multiply that across ten functions and you have ten slightly different, slightly buggy implementations of the same plumbing, plus connection strings smeared across your code.

What breaks without this discipline is subtle and expensive. A hand-written queue consumer that acks a message before the database write completes loses data on a crash. One that opens a CosmosClient per invocation exhausts SNAT ports under load. One that swallows a transient 429 Too Many Requests drops orders silently. One that hard-codes a connection string ships a secret to git history. These are the four most common ways a roll-your-own serverless integration fails in production, and the binding model makes each of them the default-safe path instead.

Who hits this: anyone building event-driven or batch pipelines on Azure serverless — order processing, image-resize fan-out, IoT telemetry landing, webhook ingestion, ETL staging. The queue-in/database-out shape is the single most common Functions pattern, and Cosmos DB is its most common sink because both scale elastically and bill per use. If you are studying for AZ-204 (Developing Solutions for Microsoft Azure), triggers and bindings are a guaranteed exam topic, and the Storage-Queue-to-Cosmos pipeline is the canonical worked example. This tutorial is that example, built for real.

To frame the whole pipeline before the deep dive, here is each moving part, what it does, and the one thing that breaks if you get it wrong:

Part Role in the pipeline Declared as What breaks if misconfigured
Queue trigger Wakes the function when a message lands type: queueTrigger, direction: in Wrong queueName → function never fires
Queue message The work item (JSON or string) Method parameter Non-JSON body → deserialisation 500s, message poisons
Function body Transforms message → document Your code Throws on bad data → 5 retries → poison queue
Cosmos output Writes the returned object as an item type: cosmosDB, direction: out Missing id/partition key → write rejected
connection setting Names the app setting holding the auth App setting key, not the value Setting absent → host fails to start the trigger
Host (Functions runtime) Polls, batches, retries, scales host.json + plan Bad batchSize → throughput or memory issues

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already know what an Azure Function is — a small piece of code the platform runs in response to an event, billed per execution — and ideally have written a basic HTTP-triggered function. If triggers and bindings are brand new, read Azure Functions triggers and bindings explained for beginners first; this article assumes you grasp that a trigger starts a function and a binding connects it to data. You should be comfortable running az in Cloud Shell, reading JSON output, and editing a JSON or code file. Basic familiarity with Azure Storage (the Azure Storage account fundamentals — accounts, queues, blobs) and with Cosmos DB (Azure Cosmos DB APIs explained — we use the NoSQL / Core API throughout) helps, but the lab provisions both from scratch.

This sits in the Serverless integration track. It is the practical counterpart to the conceptual Azure Functions serverless patterns overview — that piece tells you which pattern to reach for; this one builds the most common one brick by brick. It pairs with managed identity: system-assigned vs user-assigned patterns, because the secure way to wire bindings is identity, not keys, and with Azure Key Vault: secrets, keys and certificates if you must keep a connection string. If your source is a richer broker than a queue, the Service Bus queue with a .NET sender and receiver walkthrough is the next step up, and its trigger binding works identically to the one here. For deploying the Bicep, the deploy your first Bicep file from scratch primer covers the az deployment group create mechanics.

A quick map of who owns what during a binding incident, so you debug in the right place:

Layer What lives here Failure classes it causes
Queue (Storage) Messages, visibility timeout, poison queue Function never fires (wrong name); poison loop
Function host Trigger polling, batching, scaling, retries Trigger won’t start (missing connection); throughput
Binding config type/direction/connection/names Silent no-op; deserialisation 500
Function code The transform logic Throws → retry → poison; bad output shape
Cosmos DB Items, partition keys, RU/s 429 throttling; partition-key rejection
Identity / RBAC Managed identity, data-plane roles Auth denied (403) on read or write

Core concepts

Five ideas make every later step obvious.

A binding is a declarative connection between your function and a data source. Instead of writing code to talk to a service, you declare a binding — its type (the service, e.g. queueTrigger, cosmosDB), its direction (in for data flowing into your function, out for data flowing out), and a connection setting that names where the credentials live. The Functions runtime reads that declaration and does the I/O for you: it fetches the queue message and hands it to your parameter, or it takes your returned object and writes it to Cosmos. Your code never constructs a QueueClient or CosmosClient.

A trigger is a special input binding that also starts the function. Every function has exactly one trigger. An ordinary input binding (direction: in) fetches reference data when the function already ran. A trigger does both — it is what causes the function to run at all and it delivers the triggering payload. The queue trigger fires when a message appears on a named queue, hands you the message body, and — crucially — only deletes the message after your function returns successfully. If your function throws, the message becomes visible again and is retried. This automatic, after-success acknowledgement is the single most valuable thing the trigger gives you.

Direction is the whole grammar. in means “the platform reads this and gives it to me.” out means “I produce this and the platform writes it.” A queue trigger is in. A Cosmos output is out. You can also have a Cosmos input binding (in) that looks up an existing document by id, and a queue output binding (out) that posts a new message — bindings compose freely. This tutorial uses a queue trigger (in) and a Cosmos output (out); that is the queue-to-database pipeline.

The connection field names a setting, not a secret. A binding’s connection property is the name of an app setting, never the connection string itself. Write "connection": "StorageConnection" and the runtime looks up an app setting called StorageConnection. That indirection keeps secrets out of function.json and your repo, and lets the same binding use a connection string in dev and a managed identity in production (identity-based connections use a prefix like StorageConnection__serviceUri). Get this name wrong and the host cannot start the trigger — a very common first-run failure.

The binding does the marshalling, so the message shape and the document shape matter. A queue message is text. If your function parameter is a string, you get the raw text; if it is a typed object (a POCO / dataclass / plain object), the runtime deserialises the JSON for you — and a message that is not valid JSON will fail to bind and poison after retries. On the output side, the object you hand the Cosmos binding becomes a document, so it must carry an id (or the binding generates one) and a value for the container’s partition key. The contract is invisible until it breaks, which is why the troubleshooting section spends most of its time here.

The binding vocabulary in one table

Pin these down before the deep sections; the glossary repeats them for lookup:

Concept One-line definition Where it lives Why it matters here
Trigger The one binding that starts the function function.json / attribute Queue trigger fires on a message
Input binding Data the platform reads in for you (in) Binding config Cosmos lookup by id (optional)
Output binding Data the platform writes out for you (out) Binding config Writes your object to Cosmos
direction in or out Each binding The entire grammar of bindings
connection App-setting name holding auth Binding config Indirection keeps secrets out of code
queueName The queue the trigger watches Trigger config Wrong name → never fires
Partition key The field Cosmos shards on Cosmos container + each item Missing it → write rejected
Poison queue <name>-poison for repeated failures Storage, auto-created Where bad messages land after retries
maxDequeueCount Retries before a message poisons host.json Default 5; tune for flaky work
batchSize Messages fetched per poll host.json Throughput vs concurrency knob
Isolated worker .NET runs out-of-process from host Project model The current, recommended .NET model

The binding contract: function.json, attributes, and decorators

Bindings are declared differently per language, but the fields are identical because they all compile to the same runtime contract. The canonical function.json makes every language click, so start there even if you write C#.

function.json — the canonical declaration (script languages)

For script-based languages (JavaScript, PowerShell, Python v1) each function is a folder with a function.json listing its bindings. For our pipeline:

{
  "bindings": [
    {
      "name": "orderMessage",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "orders",
      "connection": "StorageConnection"
    },
    {
      "name": "orderDocument",
      "type": "cosmosDB",
      "direction": "out",
      "databaseName": "ShopDB",
      "containerName": "orders",
      "createIfNotExists": false,
      "partitionKey": "/customerId",
      "connection": "CosmosConnection"
    }
  ]
}

Read it top to bottom: a queue trigger named orderMessage watches the orders queue using the StorageConnection setting; a Cosmos output named orderDocument writes to the orders container in ShopDB using CosmosConnection. The name values become the parameters/return your code sees. Every required field and its meaning:

Field Applies to Required? Meaning Gotcha
name All Yes The variable name in your code Must match the code parameter exactly
type All Yes The binding type queueTrigger not queue for the trigger
direction All Yes in or out Trigger is always in
connection Both Yes App-setting name for auth Not the connection string itself
queueName Queue Yes Queue to watch / write Lowercase, 3–63 chars, no underscores
databaseName Cosmos Yes Target database Must already exist (unless creating)
containerName Cosmos Yes Target container Older runtimes call it collectionName
partitionKey Cosmos If creating Partition key path Needed when createIfNotExists is true
createIfNotExists Cosmos No Auto-create the container Default false; true provisions at 400 RU/s

C# isolated worker — attributes

The modern .NET model is the isolated worker process (your code runs in its own process, decoupled from the host runtime — the recommended model for .NET 6+). Bindings are C# attributes; there is no function.json (it is generated at build). The same pipeline:

using Microsoft.Azure.Functions.Worker;

public class ProcessOrder
{
    [Function("ProcessOrder")]
    [CosmosDBOutput("ShopDB", "orders",
        Connection = "CosmosConnection",
        PartitionKey = "/customerId",
        CreateIfNotExists = false)]
    public OrderDocument Run(
        [QueueTrigger("orders", Connection = "StorageConnection")] Order order,
        FunctionContext context)
    {
        var log = context.GetLogger("ProcessOrder");
        log.LogInformation("Processing order {id} for {cust}", order.OrderId, order.CustomerId);

        return new OrderDocument
        {
            id = order.OrderId,            // Cosmos item id
            customerId = order.CustomerId, // partition key value
            total = order.Total,
            status = "received"
        };
    }
}

The [QueueTrigger] attribute is the trigger; the [CosmosDBOutput] attribute on the method declares the return value goes to Cosmos. The runtime deserialises the queue JSON into your Order POCO and serialises the returned OrderDocument into a Cosmos item. Note id and customerId (the partition key) on the output — non-negotiable.

Python v2 — decorators

The Python v2 programming model uses decorators on a single file, no function.json:

import azure.functions as func

app = func.FunctionApp()

@app.function_name(name="ProcessOrder")
@app.queue_trigger(arg_name="msg", queue_name="orders",
                   connection="StorageConnection")
@app.cosmos_db_output(arg_name="doc", database_name="ShopDB",
                      container_name="orders",
                      connection="CosmosConnection",
                      create_if_not_exists=False)
def process_order(msg: func.QueueMessage, doc: func.Out[func.Document]) -> None:
    body = msg.get_json()  # raises if not valid JSON
    doc.set(func.Document.from_dict({
        "id": body["orderId"],
        "customerId": body["customerId"],  # partition key
        "total": body["total"],
        "status": "received"
    }))

Here the output is an explicit func.Out[Document] parameter you .set(), rather than a return value. The three models are interchangeable in behaviour; pick by language. How they line up:

Concern function.json (script) C# isolated (attributes) Python v2 (decorators)
Trigger declaration type: queueTrigger [QueueTrigger("orders")] @app.queue_trigger(...)
Output declaration type: cosmosDB, direction: out [CosmosDBOutput(...)] on method @app.cosmos_db_output(...)
Message → object Auto-deserialise to param type Auto-deserialise to POCO msg.get_json()
Writing the doc Return / set the named output return the object doc.set(...)
Where config lives function.json file Compiled from attributes Compiled from decorators
Connection setting connection field Connection = property connection= kwarg

How the queue trigger actually behaves

The trigger is more than “run when a message arrives” — its polling, locking, retry and scaling semantics decide your throughput and your data safety. Knowing them prevents the two worst mistakes: assuming exactly-once delivery, and tuning blindly.

Polling, visibility, and at-least-once delivery

The host polls the queue on an exponential back-off (fast when busy, backing off toward a ceiling — default max 1 minute — when idle). When messages exist it fetches up to batchSize of them and runs your function concurrently for each. While a message is being processed it is invisible (the visibility timeout), so no other host instance grabs it. On success the host deletes the message; on failure (your function throws or times out) it does nothing, the visibility timeout lapses, and the message reappears for another attempt.

This gives you at-least-once delivery, never exactly-once. If your function writes to Cosmos then crashes before the host deletes the message, the message is redelivered and you write the document again. The defence is idempotency: use a deterministic id (e.g. the order id) so the second write is an upsert of the same item, not a duplicate. Bindings make at-least-once easy and exactly-once impossible — design for it.

Retries, the poison queue, and dequeue count

Each redelivery increments the message’s dequeue count. When it exceeds maxDequeueCount (default 5), the host stops retrying and moves the message to a poison queue named <queueName>-poison (here, orders-poison), auto-created on first use. The poison queue is your dead-letter: bad payloads, persistent bugs, and downstream outages that outlast five attempts all land there for inspection instead of looping forever and burning money. The knobs that govern all of this live in host.json:

Setting (host.json) Controls Default Range / note When to change
batchSize Messages fetched per poll, run concurrently 16 1–32 Lower for heavy/memory-bound work; higher for tiny messages
newBatchThreshold Fetch a new batch when in-flight drops below this batchSize/2 ≥0 Raise to keep the pipeline fuller
maxDequeueCount Retries before poison 5 ≥1 Raise for flaky downstreams; lower to fail fast
visibilityTimeout How long a message stays hidden while processing 0 (immediate retry) up to 7 days Set to back off between retries
maxPollingInterval Idle back-off ceiling 1 min 1s–7 days Lower for snappier pickup on bursty low traffic

A worked intuition: with batchSize: 16 and newBatchThreshold: 8, a single instance keeps up to 24 messages in flight. On Consumption the platform also scales out instances by queue depth, so total concurrency is instances × (batchSize + newBatchThreshold). That is why a Functions queue consumer absorbs spikes a single VM never could.

Message types your parameter can be

The runtime adapts the message to your parameter type. Choosing the right one avoids manual parsing:

Parameter type You receive Use when
string Raw message text Message is plain text, or you parse yourself
POCO / class / dict Deserialised JSON object Message is JSON (the common case)
byte[] Raw bytes Binary payloads
QueueMessage (SDK type) Full envelope: id, dequeue count, insertion time You need metadata (e.g. retry count)
BinaryData / JObject Loosely-typed JSON Schema varies per message

If you bind to a POCO and the message is not valid JSON, binding fails before your code runs, the attempt counts as a failure, and after maxDequeueCount the message poisons. That is the most common “my function never logs anything but messages disappear” mystery — the failure is in the bind, not your body.

How the Cosmos DB output binding actually behaves

The output binding turns a returned object into a Cosmos item via an upsert. Two things decide whether it succeeds: the item shape and the container’s throughput.

The item shape: id and partition key are mandatory

Cosmos requires every item to have an id (unique within a logical partition) and a value for the container’s partition key path. The output binding uses the id on your object if present (otherwise generates a GUID) and reads the partition-key value from the field the container’s key path points at (/customerId → the object’s customerId).

If your object lacks the partition-key field, the write is rejected — the item has nowhere to shard. If two objects share an id and partition key, the binding upserts (overwrites), exactly what you want for idempotent redelivery. Make id the natural business key (order id, event id) so retries are safe. The binding’s key properties:

Binding property Meaning Default Gotcha
databaseName Target database — (required) Must exist unless createIfNotExists
containerName Target container — (required) Older API: collectionName
partitionKey Key path, used only when creating Item must carry a value at this path regardless
createIfNotExists Auto-create container false Creates at 400 RU/s manual throughput
connection App-setting name for auth — (required) Connection string or identity prefix
preferredLocations Region hint for multi-region accounts none Set for geo-distributed reads/writes

Throughput, 429s, and why the binding retries

Every Cosmos write costs Request Units (RU/s) — a normalised currency for throughput. A small (~1 KB) point write costs roughly 5 RU. If your incoming write rate exceeds the container’s provisioned RU/s, Cosmos returns 429 Too Many Requests with a x-ms-retry-after-ms header. The Cosmos output binding (via the SDK underneath) automatically retries 429s up to a bounded count, honouring the retry-after — so transient throttling is handled for you. But sustained over-rate still surfaces as failures, which means the message eventually poisons. The fixes are real Cosmos levers:

Symptom Cause Fix Trade-off
Occasional 429, recovers Brief spike over provisioned RU/s Binding’s built-in retry handles it None — expected
Sustained 429, messages poison Write rate > provisioned RU/s for minutes Raise manual RU/s, or use autoscale RU/s Higher cost; autoscale bills up to max
429 only at flash-sale peaks Bursty, hard to provision flat Autoscale (scales 10%→100% of max) Pay for the peak ceiling briefly
Hot partition 429 while account underused Skewed partition key Redesign partition key for even spread Schema/migration effort

Partition-key choice is the highest-leverage Cosmos decision and it lives in your transform. A good key (high cardinality, even access — customerId, tenantId) spreads load; a bad one (a constant, or status with three values) creates hot partitions that throttle even when the account has spare RU/s. The binding cannot fix a bad key — you choose it when you shape the document.

Wiring the connection: keys vs managed identity

The connection setting is where security is won or lost. Two ways to authenticate a binding to its backing service.

Connection string (simplest, least secure). The app setting holds the full secret — a storage account key or a Cosmos primary key. Easy to start with, but the secret sits in app settings, can leak, and must be rotated manually. Use it only for local dev or a quick lab.

Identity-based connection (recommended). The Function App gets a managed identity, is granted a data-plane RBAC role on the storage and Cosmos accounts, and the binding authenticates as that identity — no secret anywhere. Instead of one setting holding a string, you set a prefix of settings pointing at the resource by URI:

# Identity-based storage connection: a prefix, not a connection string
StorageConnection__serviceUri = https://stshopprod.queue.core.windows.net
StorageConnection__credential = managedidentity     # (optional; implied for MI)

# Identity-based Cosmos connection
CosmosConnection__accountEndpoint = https://cosmos-shop-prod.documents.azure.com:443/

The runtime sees the __serviceUri / __accountEndpoint suffix and switches to token auth via the identity. The roles you must grant (data-plane, not control-plane — a common trip-up):

Service Role for this pipeline What it allows Note
Storage Queue (trigger) Storage Queue Data Message Processor Get + delete messages (peek-lock semantics) Reader role can’t delete → message never acks
Storage Queue (also) Storage Queue Data Contributor Full message R/W incl. add Use if the function also enqueues
Cosmos DB (output) Cosmos DB Built-in Data Contributor Read + write items (data plane) Assigned via az cosmosdb sql role assignment, not IAM blade
Blob (host storage) Storage Blob Data Owner Host’s AzureWebJobsStorage internals Needed if the host storage itself is identity-based

A critical gotcha: Cosmos data-plane RBAC is separate from Azure IAM. You assign it with az cosmosdb sql role assignment create, not the portal’s Access Control (IAM) blade — “Contributor” in IAM gives control-plane rights (manage the account) but zero data access, so the binding still gets 403. This catches almost everyone the first time.

Architecture at a glance

Walk the diagram left to right and you have read the whole pipeline. A producer — any app, a webhook, a load test, even you with one az command — drops a JSON order message onto the orders queue in a standard storage account. The message sits there durably (up to 7 days by default) until the Function App’s queue trigger notices it. The trigger fetches a batch, marks each message invisible, and invokes your ProcessOrder function once per message, handing it the deserialised order. Your code does the only real work — shaping the order into a document with an id and a customerId partition key — and returns it. The Cosmos DB output binding upserts that document into the orders container in the ShopDB database. The instant your function returns without throwing, the host deletes the queue message; if it threw, the message reappears and is retried up to five times before landing in the orders-poison queue.

Two cross-cutting concerns ride alongside the happy path. Identity sits to the left of both data stores: the Function App’s managed identity carries the Storage Queue Data Message Processor role (to get and delete messages) and the Cosmos DB Built-in Data Contributor role (to write items), so no key appears in code or config. Observability sits to the right: every invocation, the queue depth, the Cosmos RU charge, and any 429 or poison event flow into Application Insights / Azure Monitor, which is where you confirm the pipeline is healthy and where you catch a poison build-up before it becomes a backlog. The numbered badges mark the five hops where this pipeline breaks in practice, and the legend names each one as symptom · how to confirm · fix.

Left-to-right Azure architecture: a producer enqueues a JSON order onto the orders Storage Queue; an Azure Function App queue trigger fetches and deserialises it; the function transforms it and a Cosmos DB output binding upserts the document into the orders container in ShopDB; a managed identity grants Storage Queue Data Message Processor and Cosmos DB Built-in Data Contributor roles; Application Insights captures invocations, RU charges, 429s and poison-queue events; numbered badges mark the queue-name mismatch, JSON deserialisation/poison, missing partition key, Cosmos 429 throttling, and identity/RBAC denial failure points.

Real-world scenario

Northwind Retail runs a flash-sale platform. On a normal day their storefront posts a few hundred orders an hour; during a sale, 8,000 orders land in ninety seconds. The original design was a single Linux VM running a Node worker that polled a Storage Queue, built a CosmosClient per message, wrote the order, then deleted the message. It worked in testing. The first real sale took it down in four minutes.

Three things failed at once. First, the worker created a new CosmosClient on every invocation; under burst, the VM exhausted outbound connections and SNAT ports, and writes timed out. Second, because the code deleted the message before confirming the Cosmos write under load (a race introduced in a “performance” refactor), roughly 1.2% of orders were acknowledged but never persisted — silent data loss found only in reconciliation the next morning. Third, the single VM could not scale with queue depth, so the backlog grew to 40 minutes and customers saw “order pending” spinners until it drained.

The rebuild replaced the worker with a single Azure Function on a Consumption plan: a queue trigger on orders and a Cosmos output binding to the orders container. The function body went from ~70 lines to 9 — read the order, set id = orderId and customerId as the partition key, return the document. Four wins fell out of the binding model for free. The platform now deletes the message only after the function returns successfully, eliminating the ack-before-write data loss outright. The Cosmos binding reuses a pooled, host-managed client, so the per-invocation connection storm vanished. The Consumption plan scaled to 47 instances at peak based on queue depth and drained the 8,000-order burst in under two minutes. And making id the deterministic order id turned the inevitable at-least-once redeliveries into harmless upserts instead of duplicates.

The one real tuning task was Cosmos throughput. The first sale threw a wave of 429s because the container ran a flat 1,000 RU/s. They switched it to autoscale RU/s with a 10,000 max; outside sales it idles near the floor, and during a sale it scales to absorb the write rate, the binding’s built-in 429 retry smoothing the brief overshoots. Total ingestion cost dropped versus the always-on VM, because Consumption Functions and autoscale Cosmos both bill for the burst and idle cheap. The lesson on the wall: the binding gave us correctness for free; we only had to size the database.

Advantages and disadvantages

Bindings are a strong default, but they are not free of trade-offs. The honest two-column view:

Advantages Disadvantages
No client/connection code — far less to write and get wrong Less control over the client (timeouts, custom serialisers, policies)
Secrets stay out of code via the connection indirection Identity-based setup (roles) has real first-time friction
Message deleted only after success → at-least-once safety At-least-once means you must design for idempotency yourself
Host pools and reuses clients → no per-call connection storm Harder to share one tuned client across many functions
Poison queue + retries built in → no dead-letter plumbing Poison handling is fixed-ish; complex DLQ logic needs code
Scales with queue depth on Consumption automatically Cold starts on Consumption add first-message latency
Declarative config is readable and reviewable A one-character config typo fails silently (never fires)
Same model across queues, blobs, Cosmos, Service Bus, Event Hubs Niche operations (bulk, transactional batch) need the SDK directly

When the advantages dominate: standard event-driven and batch pipelines — queue/blob/event in, database/queue out — where the transform is simple and throughput is bursty. This is the 90% case, and bindings win decisively. When to drop to the SDK: bulk or transactional-batch Cosmos writes, custom retry/circuit-breaker policies, a shared pre-tuned client, or fine serialisation control. You can mix — keep the trigger binding to start and acknowledge safely, but write to Cosmos with an injected CosmosClient when you need bulk semantics. The trigger’s safety is the part you almost never give up; the output binding is the part you sometimes outgrow.

Hands-on lab

This is the centrepiece. You will build the full pipeline three ways and run it end to end. Pick the portal path or the CLI path to provision (they produce the same thing); the Bicep version is a reproducible bonus. Then deploy the function, enqueue a message, and verify the document landed. Every step states the expected output so you know it worked. Teardown is at the end — do it, and the lab costs effectively nothing.

Lab prerequisites:

Requirement How to get it Verify
Azure subscription Free account works az account show returns your sub
Azure CLI ≥ 2.55 (or Cloud Shell) az upgrade az version
Functions Core Tools v4 npm i -g azure-functions-core-tools@4 func --version → 4.x
.NET 8 SDK or Python 3.11 per your chosen language dotnet --version / python --version
A region pick one near you used as $LOC below

Set shared variables once (CLI users):

RG=rg-fnbind-lab
LOC=centralindia
SA=stfnbind$RANDOM           # storage: globally unique, lowercase, ≤24 chars
COSMOS=cosmos-fnbind-$RANDOM # cosmos account: globally unique
FUNC=func-fnbind-$RANDOM     # function app: globally unique
echo "SA=$SA COSMOS=$COSMOS FUNC=$FUNC"   # note these — you'll reuse them

Part A — Provision with the Azure portal (click path)

If you prefer CLI, skip to Part B; the result is identical.

  1. Create the resource group. Portal → Resource groupsCreate. Name rg-fnbind-lab, pick your region, Review + createCreate. Expected: the group appears in Resource groups within a few seconds.

  2. Create the storage account. Create a resourceStorage account. Resource group rg-fnbind-lab; name a unique lowercase value (e.g. stfnbindlab01); Redundancy LRS (cheapest, fine for a lab); leave the rest default; Review + createCreate. Expected: deployment succeeds; the account opens.

  3. Create the queue. Open the storage account → Data storageQueues+ Queue. Name it orders (lowercase, 3–63 chars) → OK. Expected: orders shows in the queue list, 0 messages.

  4. Create the Cosmos DB account. Create a resourceAzure Cosmos DBAzure Cosmos DB for NoSQLCreate. Resource group rg-fnbind-lab; account name a unique value (e.g. cosmos-fnbind-lab01); Capacity mode Provisioned throughput; check Apply Free Tier Discount if available (1,000 RU/s + 25 GB free); Review + createCreate. This takes a few minutes. Expected: the account deploys; Data Explorer opens.

  5. Create the database and container. In Data ExplorerNew Container. Database id ShopDB (create new); Container id orders; Partition key /customerId; throughput Manual 400 RU/s (or autoscale 1000 max). OK. Expected: ShopDB > orders appears in the tree.

  6. Create the Function App. Create a resourceFunction AppConsumption hosting. Resource group rg-fnbind-lab; a unique app name; Runtime stack your language (.NET 8 isolated, or Python 3.11); region as before; it will create or reuse a storage account (point it at stfnbindlab01). Review + createCreate. Expected: the Function App deploys; its Overview shows Running.

  7. Add the two app settings. Function App → SettingsEnvironment variables (or Configuration) → + Add. Add StorageConnection = the storage account’s connection string (Storage account → Access keys → Connection string), and CosmosConnection = the Cosmos Primary Connection String (Cosmos account → Keys). Apply. Expected: both settings list under App settings; the app restarts.

  8. Enable Application Insights. Function App → SettingsApplication InsightsTurn on (or it was created with the app). Apply. Expected: a linked Application Insights resource appears.

Provisioning is done. Jump to Part D to deploy the function code.

Part B — Provision with az CLI (scripted path)

  1. Create the resource group.
az group create --name $RG --location $LOC

Expected: JSON with "provisioningState": "Succeeded".

  1. Create the storage account and the queue.
az storage account create --name $SA --resource-group $RG \
  --location $LOC --sku Standard_LRS --kind StorageV2

# Create the orders queue (auth via the account key for the lab)
KEY=$(az storage account keys list -g $RG -n $SA --query "[0].value" -o tsv)
az storage queue create --name orders --account-name $SA --account-key "$KEY"

Expected: the account JSON shows Succeeded; the queue command returns { "created": true }.

  1. Create the Cosmos account, database, and container.
# NoSQL (Core) API account; --enable-free-tier if you have no free-tier account yet
az cosmosdb create --name $COSMOS --resource-group $RG \
  --locations regionName=$LOC --default-consistency-level Session

az cosmosdb sql database create --account-name $COSMOS \
  --resource-group $RG --name ShopDB

az cosmosdb sql container create --account-name $COSMOS \
  --resource-group $RG --database-name ShopDB \
  --name orders --partition-key-path "/customerId" --throughput 400

Expected: three Succeeded results. The container create echoes "partitionKey": { "paths": ["/customerId"] }.

  1. Create a storage account for the Function host, then the Function App. (A Function App needs its own backing storage for the host; reuse $SA to keep it simple.)
az functionapp create --name $FUNC --resource-group $RG \
  --storage-account $SA --consumption-plan-location $LOC \
  --runtime dotnet-isolated --runtime-version 8 --functions-version 4
# For Python: --runtime python --runtime-version 3.11 --os-type Linux

Expected: JSON ending in Succeeded; "state": "Running".

  1. Wire the two binding connections as app settings.
STORAGE_CS=$(az storage account show-connection-string -g $RG -n $SA -o tsv)
COSMOS_CS=$(az cosmosdb keys list --type connection-strings -g $RG -n $COSMOS \
  --query "connectionStrings[0].connectionString" -o tsv)

az functionapp config appsettings set --name $FUNC --resource-group $RG \
  --settings "StorageConnection=$STORAGE_CS" "CosmosConnection=$COSMOS_CS"

Expected: the command prints the full app-settings array including StorageConnection and CosmosConnection.

  1. (Recommended) Switch to managed identity instead of keys. Assign the app a system identity and grant the two data-plane roles, then point the bindings at URIs:
# 1. Give the Function App a system-assigned identity
PRINCIPAL=$(az functionapp identity assign -g $RG -n $FUNC --query principalId -o tsv)

# 2. Storage Queue role (get + delete messages)
SA_ID=$(az storage account show -g $RG -n $SA --query id -o tsv)
az role assignment create --assignee $PRINCIPAL \
  --role "Storage Queue Data Message Processor" --scope $SA_ID

# 3. Cosmos DATA-PLANE role (NOT the IAM blade) — built-in Data Contributor = 00000000-0000-0000-0000-000000000002
COSMOS_ID=$(az cosmosdb show -g $RG -n $COSMOS --query id -o tsv)
az cosmosdb sql role assignment create --account-name $COSMOS -g $RG \
  --role-definition-id 00000000-0000-0000-0000-000000000002 \
  --principal-id $PRINCIPAL --scope $COSMOS_ID

# 4. Repoint the bindings at URIs (identity-based connection)
QUEUE_URI=$(az storage account show -g $RG -n $SA --query "primaryEndpoints.queue" -o tsv)
COSMOS_EP=$(az cosmosdb show -g $RG -n $COSMOS --query documentEndpoint -o tsv)
az functionapp config appsettings set -g $RG -n $FUNC --settings \
  "StorageConnection__serviceUri=$QUEUE_URI" \
  "CosmosConnection__accountEndpoint=$COSMOS_EP"
# Then remove the plain connection strings so the binding uses identity:
az functionapp config appsettings delete -g $RG -n $FUNC \
  --setting-names StorageConnection CosmosConnection

Expected: the role assignments return JSON with the role name and principal; the final settings list shows the __serviceUri and __accountEndpoint keys. Allow a couple of minutes for RBAC to propagate before the first run.

Part C — Provision with Bicep (reproducible bonus)

Save as main.bicep and deploy with az deployment group create. This provisions everything except the function code, using identity-based connections by default.

@description('Location for all resources')
param location string = resourceGroup().location
@description('Globally-unique base name')
param baseName string = 'fnbind${uniqueString(resourceGroup().id)}'

var storageName = toLower(take(replace('st${baseName}', '-', ''), 24))
var cosmosName  = toLower('cosmos-${baseName}')
var funcName    = toLower('func-${baseName}')
var planName    = 'plan-${baseName}'

// --- Storage account + orders queue ---
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}
resource queueSvc 'Microsoft.Storage/storageAccounts/queueServices@2023-05-01' = {
  parent: storage
  name: 'default'
}
resource ordersQueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2023-05-01' = {
  parent: queueSvc
  name: 'orders'
}

// --- Cosmos DB (NoSQL) account + ShopDB + orders container ---
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
  name: cosmosName
  location: location
  kind: 'GlobalDocumentDB'
  properties: {
    databaseAccountOfferType: 'Standard'
    consistencyPolicy: { defaultConsistencyLevel: 'Session' }
    locations: [ { locationName: location, failoverPriority: 0 } ]
  }
}
resource shopDb 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15' = {
  parent: cosmos
  name: 'ShopDB'
  properties: { resource: { id: 'ShopDB' } }
}
resource ordersContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
  parent: shopDb
  name: 'orders'
  properties: {
    resource: {
      id: 'orders'
      partitionKey: { paths: [ '/customerId' ], kind: 'Hash' }
    }
    options: { throughput: 400 }
  }
}

// --- Consumption plan + Function App with a system identity ---
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: planName
  location: location
  sku: { name: 'Y1', tier: 'Dynamic' }
}
resource func 'Microsoft.Web/sites@2023-12-01' = {
  name: funcName
  location: location
  kind: 'functionapp'
  identity: { type: 'SystemAssigned' }
  properties: {
    serverFarmId: plan.id
    siteConfig: {
      appSettings: [
        { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
        { name: 'FUNCTIONS_WORKER_RUNTIME', value: 'dotnet-isolated' }
        { name: 'AzureWebJobsStorage', value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' }
        // Identity-based binding connections (no secrets)
        { name: 'StorageConnection__serviceUri', value: storage.properties.primaryEndpoints.queue }
        { name: 'CosmosConnection__accountEndpoint', value: cosmos.properties.documentEndpoint }
      ]
    }
  }
}

// --- Data-plane role: Storage Queue Data Message Processor for the func identity ---
var queueProcessorRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')
resource queueRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storage.id, func.id, 'queue-processor')
  scope: storage
  properties: {
    roleDefinitionId: queueProcessorRoleId
    principalId: func.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

// --- Cosmos DATA-PLANE role assignment (built-in Data Contributor) ---
resource cosmosDataRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = {
  parent: cosmos
  name: guid(cosmos.id, func.id, 'data-contributor')
  properties: {
    roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002'
    principalId: func.identity.principalId
    scope: cosmos.id
  }
}

output functionAppName string = func.name
output cosmosEndpoint string = cosmos.properties.documentEndpoint

Deploy and verify:

az deployment group create -g $RG --template-file main.bicep
az deployment group show -g $RG -n main --query "properties.provisioningState" -o tsv

Expected: Succeeded, and the outputs print the function app name and Cosmos endpoint.

The Bicep parameters and what to tune:

Param / value Purpose Default Change when
baseName Unique seed for all names uniqueString(rg) You want readable fixed names
Storage sku Redundancy Standard_LRS Prod → Standard_ZRS/GRS
Container throughput Cosmos RU/s 400 Raise, or swap to autoscaleSettings
Plan sku Hosting tier Y1 Dynamic (Consumption) Premium for no cold start / VNet
Identity type Auth model SystemAssigned UserAssigned to share across apps

Part D — Deploy the function code

Now the code, regardless of how you provisioned. We show the C# isolated path; Python steps are noted inline.

  1. Scaffold the project.
mkdir fnbind-lab && cd fnbind-lab
func init . --worker-runtime dotnet-isolated      # Python: --worker-runtime python

Expected: a project with host.json and Program.cs (C#) or function_app.py (Python).

  1. Add the function with both bindings. For C#, create ProcessOrder.cs with the isolated-worker code from the binding contract section (the [QueueTrigger("orders")] + [CosmosDBOutput(...)] example), and add the Order/OrderDocument model classes plus the Cosmos extension package:
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.CosmosDB
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues

For Python, paste the decorator function from the contract section into function_app.py. Expected: the project builds (dotnet buildBuild succeeded).

  1. Set maxDequeueCount and batch tuning in host.json (optional but instructive):
{
  "version": "2.0",
  "extensions": {
    "queues": {
      "batchSize": 16,
      "maxDequeueCount": 5,
      "visibilityTimeout": "00:00:30"
    }
  }
}
  1. Deploy to the Function App.
func azure functionapp publish $FUNC

Expected: output ending with Functions in $FUNC: and ProcessOrder - [queueTrigger]. If you see that line, the trigger registered.

  1. Confirm the function is listed and enabled.
az functionapp function list -g $RG -n $FUNC --query "[].{name:name, disabled:config.disabled}" -o table

Expected: ProcessOrder with disabled false.

Part E — Run the pipeline end to end

  1. Enqueue a test order. The message must be valid JSON matching your model. Storage Queue messages are base64 by default for the SDK; the CLI handles encoding:
az storage message put --queue-name orders --account-name $SA --account-key "$KEY" \
  --content '{"orderId":"ord-1001","customerId":"cust-42","total":129.50}'

Expected: JSON describing the queued message (an id, insertionTime, expirationTime).

  1. Watch the function fire (live logs).
func azure functionapp logstream $FUNC
# or: az webapp log tail (for the underlying site)

Expected: within seconds, a log line Processing order ord-1001 for cust-42 and an Executed 'ProcessOrder' (Succeeded ...) entry.

  1. Confirm the document landed in Cosmos. Query the container for the item:
az cosmosdb sql query --account-name $COSMOS -g $RG \
  --database-name ShopDB --container-name orders \
  --query-text "SELECT * FROM c WHERE c.id = 'ord-1001'"

Expected: one item with id: ord-1001, customerId: cust-42, total: 129.5, status: received, plus Cosmos system fields (_rid, _ts). You can also see it in Data Explorer → ShopDB → orders → Items.

  1. Confirm the queue is empty (the message was deleted on success):
az storage message peek --queue-name orders --account-name $SA --account-key "$KEY" --num-messages 5

Expected: [] — empty. The trigger deleted the message after the function returned.

  1. (Optional) Prove the poison path. Enqueue a deliberately broken message and watch it retry then poison:
az storage message put --queue-name orders --account-name $SA --account-key "$KEY" \
  --content 'this-is-not-json'
# after ~5 attempts:
az storage message peek --queue-name orders-poison --account-name $SA --account-key "$KEY"

Expected: logs show repeated failures; after maxDequeueCount attempts the message appears in orders-poison. This is the dead-letter safety net working.

Part F — Teardown

One command removes everything and stops all billing:

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

Expected: the command returns immediately; the resource group and all resources delete in the background. Confirm with az group exists -n $RG (returns false once done). For the portal path, delete the rg-fnbind-lab resource group from Resource groups → … → Delete.

Lab artefact Cost while running Removed by teardown
Storage account + queue Pennies (LRS, tiny data) Yes
Cosmos 400 RU/s container ~$23/mo if left (free tier: $0) Yes
Function App (Consumption) $0 idle; per-execution only Yes
Application Insights Free tier covers lab volume Yes

Common mistakes & troubleshooting

These are the failures you will hit, in rough order of how often. Each: symptom → root cause → how to confirm → fix.

# Symptom Root cause Confirm (exact path/command) Fix
1 Function never fires; messages pile up queueName typo or wrong connection setting az functionapp config appsettings list — is the named setting present? Check trigger queueName vs actual queue Match the queueName and the connection app-setting name exactly
2 Messages disappear but no logs / no document Bind-time JSON deserialisation fails (message not valid JSON for the POCO) Peek orders-poison; logs show Exception binding parameter Send valid JSON; bind to string/QueueMessage to inspect raw
3 403 Forbidden writing to Cosmos Granted IAM “Contributor” not the data-plane role az cosmosdb sql role assignment list — is the principal listed? az cosmosdb sql role assignment create with ...000000000002
4 Cosmos write rejected: missing partition key Output object lacks the partition-key field Logs show PartitionKey ... doesn't exist; inspect the returned object Add the customerId (key path) field to the document
5 Intermittent failures under load, then poison Cosmos 429 throttling beyond retry budget App Insights dependencies to Cosmos with resultCode 429 Raise RU/s or switch container to autoscale
6 Host won’t start; “Microsoft.Azure.WebJobs … cannot find” Missing binding extension package func start locally shows the missing-extension error Add ...Extensions.CosmosDB / .Storage.Queues packages
7 Same message processed twice (duplicate docs) At-least-once redelivery + non-deterministic id Two items differing only by generated GUID id Make id the business key (order id) → upsert idempotency
8 Trigger works locally, silent in Azure AzureWebJobsStorage invalid, or identity lacks blob role Function App → Diagnose and solveFunction runtime down Fix AzureWebJobsStorage; grant host storage the blob role
9 collectionName vs containerName error Old binding property name on a new runtime Build/start error naming the property Use containerName (current); collectionName is legacy
10 Messages retry forever, never poison maxDequeueCount very high, or poison queue write blocked host.json value; check identity can add to orders-poison Set sane maxDequeueCount; grant Data Contributor if identity-based

Two diagnostic habits cut these in half. First, inspect the poison queue early — if messages vanish without a document, peek orders-poison before anything else; the original payload is sitting there telling you what broke. Second, for any silent no-op, suspect the binding before the body — a wrong connection name or queueName makes the trigger never register, so there is nothing in your code logs. The clue is the absence of Executed 'ProcessOrder' lines in func azure functionapp logstream.

Best practices

Security notes

The binding model is more secure than hand-rolled clients by default — but only if you take the identity path. The least-privilege posture for this pipeline:

Concern Default-safe approach What to avoid
Authentication System-assigned managed identity per Function App Connection strings / account keys in app settings
Storage queue access Storage Queue Data Message Processor (get + delete only) Account key, or over-broad Contributor
Cosmos access Cosmos DB Built-in Data Contributor, data-plane, scoped to the account Primary key in CosmosConnection; IAM Owner
Secrets (if unavoidable) Key Vault reference in the app setting Plaintext secret committed or in config
Network exposure Private endpoints for Storage + Cosmos; VNet-integrate the app Public endpoints with keys on the internet
Least privilege scope Role scoped to the specific account, not the subscription Subscription-level data roles

Three specifics worth internalising. The queue trigger needs delete, not just read — Storage Queue Data Reader lets the trigger fetch a message but not acknowledge it, so every message reprocesses forever; use Data Message Processor (the smallest role that includes delete). Cosmos data-plane RBAC is granted and listed with az cosmosdb sql role assignment list/create and is invisible in the IAM blade, so audits that only check IAM miss it — check both. And if a connection string is truly unavoidable, store it as a Key Vault reference (@Microsoft.KeyVault(SecretUri=...)) so the secret lives in the vault with rotation and access logging. See Azure Key Vault: secrets, keys and certificates for the syntax and managed identity patterns for choosing system- vs user-assigned.

Cost & sizing

This pipeline is cheap by design — both ends bill for what you use — but two line items can surprise you. Rough figures (USD; INR ≈ ×83):

Cost driver Pricing model Free-tier / idle Watch out for
Function executions Per execution + GB-seconds (Consumption) 1M executions + 400,000 GB-s free/month High-frequency tiny messages add up
Cosmos RU/s Provisioned per 100 RU/s-hour, or autoscale 1,000 RU/s + 25 GB free (one free-tier acct) Flat over-provisioning is the silent waste
Cosmos storage Per GB-month Within the 25 GB free Unbounded document growth
Storage (queue) Per GB + per 10k transactions Negligible at lab scale Huge message volume × transactions
Application Insights Per GB ingested 5 GB/month free Verbose logging at high throughput

The two real decisions. On Functions, Consumption is almost always right for queue-driven work — you pay per message and nothing at idle, matching bursty pipelines. Move to Premium only if cold-start latency on the first message after idle is unacceptable, or you need VNet integration; Premium carries an always-on baseline cost. On Cosmos, the partition-key and throughput-mode choices dominate the bill. A flat 1,000 RU/s container costs roughly $58/month whether or not you use it; the same workload on autoscale (max 1,000) bills only for the RU/s consumed each hour (down to 100 RU/s at idle), which for a bursty pipeline is far cheaper. For sizing: estimate peak writes/sec × ~5 RU per ~1 KB write, set that as the autoscale max, and let it idle down between bursts.

Interview & exam questions

Pitched for AZ-204 and senior serverless interviews. Question, then a model answer.

1. What is the difference between a trigger and an input binding? A trigger is the single binding that causes the function to run and delivers the triggering payload; every function has exactly one. An input binding (direction: in) supplies additional reference data to a function that has already been triggered. A queue trigger both starts the function and hands you the message.

2. What does direction mean, and what are the values? direction declares data flow relative to your function: in means the platform reads the source and gives it to your code; out means your code produces data the platform writes to the target. A queue trigger is in; a Cosmos output is out.

3. The connection property holds what — the connection string? No. connection is the name of an app setting that holds the credentials, never the secret itself. This indirection keeps secrets out of function.json/code and lets the same binding use a connection string in dev and a managed identity (via a __serviceUri/__accountEndpoint prefix) in production.

4. When is a Storage Queue message deleted? Only after the function returns successfully. If the function throws or times out, the message becomes visible again after its visibility timeout and is retried, giving at-least-once delivery. After maxDequeueCount failures it moves to the <queue>-poison queue.

5. Why must your Cosmos output object carry an id and a partition-key value? Cosmos requires both for every item: id is unique within a logical partition; the partition-key value tells Cosmos which shard the item belongs to. The binding generates an id if absent but cannot invent the partition-key value — without it the write is rejected.

6. How do you make at-least-once delivery safe against duplicates? Design idempotently: use a deterministic id (the business key) so a redelivered message upserts the same item rather than creating a duplicate. The binding writes via upsert, so identical id + partition key overwrites in place.

7. What is the poison queue and when is it used? A queue named <queueName>-poison, auto-created, where the host moves a message after it has failed more than maxDequeueCount times (default 5). It is the built-in dead-letter so persistently failing messages don’t loop forever.

8. You granted the Function App “Contributor” on the Cosmos account but writes still 403. Why? Cosmos data-plane access is separate from Azure IAM. “Contributor” is a control-plane role (manage the account) and grants no data access. You must assign a Cosmos data-plane role (e.g. Built-in Data Contributor) with az cosmosdb sql role assignment create.

9. How does the queue trigger scale on the Consumption plan? The Functions scale controller monitors queue depth and adds instances; each instance processes up to batchSize + newBatchThreshold messages concurrently, so total throughput is roughly instances × (batchSize + newBatchThreshold). It scales to zero when the queue is empty.

10. When would you bypass the Cosmos output binding and use the SDK directly? When you need operations the binding doesn’t expose well: bulk/transactional-batch writes, custom retry/circuit-breaker policies, a shared pre-tuned client, or fine serialisation control. A common hybrid keeps the queue trigger (for safe acknowledgement) but injects a CosmosClient for bulk writes.

11. What’s the difference between binding to string versus a typed object for a queue message? Binding to string gives you the raw text to parse yourself. Binding to a typed object makes the runtime deserialise the JSON into that type before your code runs — convenient, but a non-JSON message fails at bind time and poisons after retries, before your body executes.

12. Which host.json setting limits how many messages a single instance processes at once, and what’s the trade-off? batchSize (default 16, max 32) plus newBatchThreshold. Higher values raise per-instance concurrency and throughput but increase memory/CPU pressure per instance; lower them for heavy work, raise them for tiny messages.

Quick check

  1. A queue trigger has direction set to what value?
  2. What is the name of the queue that receives messages after they exceed maxDequeueCount?
  3. The connection field in a binding holds the connection string — true or false?
  4. Which two fields must an object carry for a Cosmos output binding to write it successfully?
  5. Granting Azure IAM “Contributor” on a Cosmos account lets a binding write items — true or false?

Answers

  1. in. A trigger is an input binding (it reads the event in and starts the function); only output bindings use out.
  2. <queueName>-poison (e.g. orders-poison), auto-created on first poison.
  3. False. It holds the name of an app setting that contains the credentials, keeping secrets out of the binding config.
  4. An id (the binding generates one if absent) and a value for the partition-key path (e.g. customerId). The partition-key value cannot be auto-generated.
  5. False. That is a control-plane role; data access needs a Cosmos data-plane role assigned via az cosmosdb sql role assignment create.

Glossary

Next steps

AzureAzure FunctionsStorage QueueCosmos DBBindingsServerlessBicepManaged 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