DEV Community

Cover image for Azure Service Bus: Queues and Topics for When Event Grid Isn't Enough
Bry
Bry

Posted on Originally published at Medium

Azure Service Bus: Queues and Topics for When Event Grid Isn't Enough

Key Points

  • Basic tier supports queues only — no topics, no subscriptions, no sessions, at any price. It's a feature gate, not a cheaper version of Standard, and Standard is the practical floor the moment any of those three enter the picture.
  • Sessions are Service Bus's actual differentiator over Event Grid: they group related messages and guarantee in-order, single-consumer processing within a session — Event Grid has no equivalent.
  • Premium tier drops per-message transaction billing entirely in favor of a flat daily rate per messaging unit — the economics flip once you're at consistent high volume.
  • A subscription with no filter rule receives every message on the topic via the implicit 1=1 default rule — easy to forget you're relying on that default.

Prerequisites

  • CLI/SDK version tested against: az-cli 2.6x.x
  • An Azure resource group (az group create)
  • Standard tier or higher for the topic/subscription/session examples below — Basic tier will reject topic creation outright

Introduction

Article 352 covered Event Grid and drew a clear line: it's a push-based router for discrete events, not a queue and not a stream. This article covers the service that actually is a queue — with topics, ordering guarantees, and transactional semantics Event Grid was never built to provide.

The single most common surprise I've watched people hit with Service Bus isn't conceptual, it's the tier structure. Someone provisions a Basic-tier namespace to save money, tries to create a topic, and gets a flat rejection — topics simply don't exist at that tier. Not throttled, not limited. Absent. That's worth knowing before you provision anything, not after.

This article builds a session-enabled queue (the feature that actually justifies choosing Service Bus over Event Grid for ordered, related work) and a filtered topic subscription, entirely from the CLI.


The Tier Gate

The Tier Gate

Diagram: Basic tier is a genuinely different feature set, not a scaled-down Standard — this is the detail that catches people off guard.

Tier Queues Topics/Subscriptions Sessions Billing model
Basic Yes No No Per-operation, lowest rate
Standard Yes Yes Yes Per-operation, moderate rate
Premium Yes Yes Yes Flat daily rate per messaging unit (1, 2, or 4 units); no per-message charge

If your design needs a topic or a session — and sessions are the reason to pick Service Bus at all in most cases — Basic tier is off the table before you write a single line of application code.


Building a Session-Enabled Queue

Sessions group related messages so they're delivered in order to exactly one consumer at a time, identified by a session ID you assign when sending. This is the feature Event Grid genuinely cannot do — it has no concept of message grouping or delivery ordering across a related set.

az servicebus namespace create \
  --resource-group rg-servicebus-demo \
  --name sb-demo-namespace \
  --location eastus \
  --sku Standard

az servicebus queue create \
  --resource-group rg-servicebus-demo \
  --namespace-name sb-demo-namespace \
  --name orders-sessions-queue \
  --enable-session true
Enter fullscreen mode Exit fullscreen mode

Building a Session-Enabled Queue<br>

Diagram: session locking guarantees in-order delivery within a session without blocking unrelated sessions from being processed concurrently by other consumers.

Sending a message into a session queue requires setting the session ID explicitly — omit it and the send fails, because --enable-session true makes session ID mandatory for every message on that queue, not optional.


Building a Filtered Topic Subscription

az servicebus topic create \
  --resource-group rg-servicebus-demo \
  --namespace-name sb-demo-namespace \
  --name orders-topic

az servicebus topic subscription create \
  --resource-group rg-servicebus-demo \
  --namespace-name sb-demo-namespace \
  --topic-name orders-topic \
  --name high-value-orders

# Without this rule, the subscription's default "1=1" filter
# receives every message on the topic — not just high-value orders.
az servicebus topic subscription rule create \
  --resource-group rg-servicebus-demo \
  --namespace-name sb-demo-namespace \
  --topic-name orders-topic \
  --subscription-name high-value-orders \
  --name HighValueFilter \
  --filter-sql-expression "Total > 500"
Enter fullscreen mode Exit fullscreen mode

Any subscription created without an explicit rule keeps the implicit default rule, which matches everything. That's fine when you genuinely want every message — the risk is forgetting the default is there at all and being surprised when a subscription you intended to be narrow is receiving the full topic firehose.


When to Reach for Service Bus Instead of Event Grid

Service Bus Event Grid
Delivery model Pull (consumer receives/completes) Push (webhook delivery)
Ordering Yes, via sessions No
Message grouping Yes, via sessions No
Duplicate detection Yes (Standard+) No
Transactions Yes (Standard+) No
Best for Ordered work queues, transactional messaging React-to-discrete-event fan-out

Recommendation: If a stakeholder describes a requirement involving "process these in order" or "these messages belong together and must not interleave," that's sessions, which means Service Bus. If the requirement is "notify several things when X happens" with no ordering constraint, Event Grid remains the lighter, cheaper choice from article 352.


Common Mistakes

Mistake 1: Provisioning Basic tier, then needing a topic
There's no in-place upgrade path that preserves existing resources across a tier change in every case — plan tier selection before provisioning, not as a fix-it-later decision.

Mistake 2: Enabling sessions without assigning session IDs on every send
--enable-session true makes the session ID mandatory. A send call missing it fails outright — this isn't a soft warning.

Mistake 3: Assuming a subscription with no rule is empty until configured
It's not empty — it has the default 1=1 rule and receives everything. If you wanted a narrow subscription, add the filter before anything starts publishing, not after noticing unexpected volume.

Mistake 4: Reaching for Service Bus when Event Grid would do
If there's no ordering or grouping requirement, Service Bus's added complexity and Standard-tier cost floor isn't buying you anything Event Grid didn't already provide cheaper.


Production Considerations

Performance: Standard tier is shared-capacity and can experience throttling under sustained high load. Premium's dedicated messaging units remove that variability — worth the flat rate once volume is consistent and predictable rather than spiky.

Security: Use Azure AD (Entra ID) authentication with managed identities rather than shared access signature connection strings where possible — connection strings embed credentials that are easy to leak into logs or source control.

Cost: Premium's flat daily rate per messaging unit becomes cheaper than Standard's per-operation billing once your message volume is consistently high — model both before committing, since Premium at low volume is a worse deal.

Monitoring: Track ActiveMessageCount and DeadLetterMessageCount per queue and subscription. A rising dead-letter count with no alert is the Service Bus equivalent of the EventBridge and Event Grid dead-letter blind spots covered in this series' earlier articles.


Full Example: Send and Receive with Sessions (TypeScript)

import { ServiceBusClient } from '@azure/service-bus';

const connectionString = process.env.SERVICEBUS_CONNECTION_STRING!;
const client = new ServiceBusClient(connectionString);

/** Sends two related messages into the same session, in order. */
async function sendSessionMessages(sessionId: string): Promise<void> {
  const sender = client.createSender('orders-sessions-queue');
  await sender.sendMessages([
    { body: { orderId: sessionId, step: 'created' }, sessionId },
    { body: { orderId: sessionId, step: 'payment-confirmed' }, sessionId },
  ]);
  await sender.close();
}

/** Receives and processes messages for a specific session in order. */
async function receiveSessionMessages(sessionId: string): Promise<void> {
  const receiver = await client.acceptSession('orders-sessions-queue', sessionId);
  const messages = await receiver.receiveMessages(10, { maxWaitTimeInMs: 5000 });

  for (const message of messages) {
    console.log(`Session ${sessionId}: ${JSON.stringify(message.body)}`);
    await receiver.completeMessage(message);
  }

  await receiver.close();
}

async function main() {
  const sessionId = 'order-abc123';
  await sendSessionMessages(sessionId);
  await receiveSessionMessages(sessionId);
  await client.close();
}

main();
Enter fullscreen mode Exit fullscreen mode

Full source with the topic/subscription example: GitHubcloud-apis/azure-service-bus-cli/


Conclusion

Reach for Service Bus over Event Grid specifically when ordering or message grouping matters — sessions are the concrete feature that justifies it, not a vague "more enterprise" gesture. Standard tier is the practical floor the moment topics or sessions enter the picture; Basic tier's queue-only feature gate isn't a limitation you can budget around, it's absent entirely. Know which one you actually need before you provision the namespace — moving between tiers after the fact costs more time than picking correctly up front.


Further Reading


If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.

Bry Writes Code — cloud and API infrastructure specialist. Choosing between Event Grid and Service Bus for your next Azure project? Get in touch.

Top comments (0)