Service Bus

Lightweight, dependency-free fake of Azure Service Bus that speaks the documented Service Bus REST API over plain HTTP, so application code can drive queues, topics, subscriptions, rules and brokered messages with zero cost and zero side effects.

KeyValue
Port4592
ProtocolAzure Service Bus REST API (HTTP + Atom/XML management, brokered-message runtime)
Compatible client@azure/service-bus (logical surface) / any HTTP client
Size~64 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Wire-transport note. The real @azure/service-bus SDK uses AMQP 1.0 for its data plane — a binary framing protocol that is intentionally out of scope for a tiny in-process fake. This fake instead implements Azure's documented HTTP/REST surface, which mirrors the same logical operations 1:1 (send / peek-lock receive / complete / abandon / renew-lock / dead-letter / defer / schedule, plus the full Atom-based management API). Agents and application code can therefore exercise every Service Bus concept without a broker. See the Supported vs unsupported table below.

Quick Start

Start the server:

import { ServicebusServer } from "./services/servicebus/src/server.js";

const server = new ServicebusServer(4592);
await server.start();
// ... use it ...
await server.stop();

Drive it over the Service Bus REST API with any HTTP client. The examples below use fetch.

Management (Atom/XML)

const base = "http://127.0.0.1:4592";
const ATOM = { "Content-Type": "application/atom+xml;type=entry;charset=utf-8" };

// Create a queue
await fetch(`${base}/orders`, {
  method: "PUT",
  headers: ATOM,
  body: `<entry xmlns="http://www.w3.org/2005/Atom"><content type="application/xml">
    <QueueDescription xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect">
      <MaxDeliveryCount>10</MaxDeliveryCount><LockDuration>PT30S</LockDuration>
    </QueueDescription></content></entry>`,
});

// Create a topic + subscription
await fetch(`${base}/events`, { method: "PUT", headers: ATOM, body: `<entry xmlns="http://www.w3.org/2005/Atom"><content type="application/xml"><TopicDescription xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect"/></content></entry>` });
await fetch(`${base}/events/subscriptions/worker`, { method: "PUT", headers: ATOM, body: `<entry xmlns="http://www.w3.org/2005/Atom"><content type="application/xml"><SubscriptionDescription xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect"/></content></entry>` });

Send + receive (brokered messages)

// Send a message (BrokerProperties carries system metadata, custom headers map
// to application properties)
await fetch(`${base}/orders/messages`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    BrokerProperties: JSON.stringify({ MessageId: "m1", Label: "new-order", CorrelationId: "c1" }),
    priority: '"high"',
  },
  body: JSON.stringify({ id: 42 }),
});

// Peek-lock receive
const recv = await fetch(`${base}/orders/messages/head?timeout=30`, { method: "POST" });
const props = JSON.parse(recv.headers.get("brokerproperties"));
const body = await recv.text();

// Complete (delete) the locked message
await fetch(`${base}/orders/messages/${props.SequenceNumber}/${props.LockToken}`, { method: "DELETE" });

Using the real @azure/service-bus client

The SDK's ServiceBusAdministrationClient (management plane) issues exactly the Atom/XML HTTP requests this fake implements. Point it at the local endpoint:

import { ServiceBusAdministrationClient } from "@azure/service-bus";

const admin = new ServiceBusAdministrationClient(
  "Endpoint=sb://127.0.0.1:4592/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=parlellocaldevkey;UseDevelopmentEmulator=true"
);
await admin.createQueue("orders");
await admin.createTopic("events");
await admin.createSubscription("events", "worker");

The data-plane ServiceBusClient (sendMessages / receiveMessages) speaks AMQP and is therefore not wire-compatible with this HTTP fake — use the REST runtime endpoints above (or raw HTTP) to send/receive against the fake.

Implemented operations

Internal (parlel)

MethodPathDescription
GET/_parlel/healthHealth check + entity counts
POST/_parlel/resetReset all in-memory state
GET/_parlel/dumpDump queues / topics / subscriptions

Management — Queues

MethodPathOperation
PUT/{queue}CreateQueue
GET/{queue}GetQueue
DELETE/{queue}DeleteQueue
GET/$Resources/QueuesListQueues

Management — Topics

MethodPathOperation
PUT/{topic}CreateTopic
GET/{topic}GetTopic
DELETE/{topic}DeleteTopic
GET/$Resources/TopicsListTopics

Management — Subscriptions

MethodPathOperation
PUT/{topic}/subscriptions/{sub}CreateSubscription
GET/{topic}/subscriptions/{sub}GetSubscription
DELETE/{topic}/subscriptions/{sub}DeleteSubscription
GET/{topic}/subscriptionsListSubscriptions

Management — Rules

MethodPathOperation
PUT/{topic}/subscriptions/{sub}/rules/{rule}CreateRule (SqlFilter / CorrelationFilter / True / False, with optional SqlRuleAction)
GET/{topic}/subscriptions/{sub}/rules/{rule}GetRule
DELETE/{topic}/subscriptions/{sub}/rules/{rule}DeleteRule
GET/{topic}/subscriptions/{sub}/rulesListRules

Every subscription is created with the default $Default TrueFilter rule.

Runtime — Messaging

MethodPathOperation
POST/{queue|topic}/messagesSend (single)
POST/{queue|topic}/messages + Content-Type: application/vnd.microsoft.servicebus.jsonSendBatch
POST/{entity}/messages/head?timeout=NReceive (peek-lock) → 201 + LockToken
DELETE/{entity}/messages/head?timeout=NReceive-and-delete → 200
DELETE/{entity}/messages/{seq}/{lockToken}Complete
PUT/{entity}/messages/{seq}/{lockToken}Abandon (unlock) — default disposition
PUT/{entity}/messages/{seq}/{lockToken} + Disposition: deadletterDead-letter
PUT/{entity}/messages/{seq}/{lockToken} + Disposition: deferDefer
POST/{entity}/messages/{seq}/{lockToken}Renew lock
POST/{entity}/messages/{sequenceNumber}Receive deferred message by sequence number

{entity} is one of:

Message semantics

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureSupported
Queue / Topic / Subscription / Rule management (Atom)
List queues / topics / subscriptions / rules
Send (single + batch)
Peek-lock receive
Receive-and-delete
Complete / Abandon / Renew-lock
Dead-letter (auto on max-delivery + explicit)
Dead-letter queue addressing ($DeadLetterQueue)
Defer + receive-by-sequence-number
Scheduled (future-enqueue) messages
Topic → subscription fan-out
SQL / Correlation / True / False filters✅ (subset of SQL grammar)
BrokerProperties + custom application properties
AMQP 1.0 binary transport (ServiceBusClient data plane)⟳ Roadmap — intentionally — REST runtime endpoints used instead
Sessions (ordered, session-locked receive)⟳ Roadmap — stored on the entity but no session-locked receive
Auto-forwarding (ForwardTo)✓ By design — accepted + persisted, not enforced
Duplicate detection✓ By design — accepted + persisted, not enforced
Shared Access Signature auth / RBAC✓ By design — all requests accepted (local fake)
Geo-disaster recovery, partitioning internals, metrics⟳ Roadmap

Error codes / shapes

Errors are returned as Azure-style XML:

<Error><Code>404</Code><Detail>MessagingEntityNotFound: The messaging entity could not be found.</Detail></Error>
HTTP statusCodeWhen
400BadRequestMalformed request / invalid JSON / sending to a subscription path
404MessagingEntityNotFoundQueue/topic/subscription/rule does not exist
409MessagingEntityAlreadyExistsCreating an entity that already exists
410LockTokenNotFoundLock token invalid or expired (complete/abandon/renew)
500InternalServerErrorUnexpected server error

Successful responses use Azure's conventions:

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

SERVICEBUS_NAMESPACE=parlel
SERVICEBUS_ENDPOINT=http://parlel-bridge:4592
SERVICEBUS_CONNECTION_STRING=Endpoint=sb://127.0.0.1:4592/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=parlellocaldevkey;UseDevelopmentEmulator=true
<!-- parlel:testenv:end -->