Architecture¶
WebMQ bridges WebSocket connections to RabbitMQ topic exchanges. Every frontend client gets a dedicated session queue on RabbitMQ, and messages flow bidirectionally through a single persistent WebSocket.
How it works¶
- A frontend connects to the WebMQ backend via WebSocket
- The backend creates a RabbitMQ session queue for that connection
- When the frontend calls
listen('orders.*', cb), the backend binds the session queue to the RabbitMQ exchange with binding keyorders.* - When any service (frontend or backend) publishes to
orders.created, RabbitMQ routes the message to all matching queues - The backend reads from the session queue and forwards messages to the frontend via WebSocket
Topic Routing¶
RabbitMQ topic exchanges route messages based on routing keys (dot-separated strings) and binding keys (which support wildcards):
order.created— exact matchorder.*— matchesorder.created,order.cancelled, etc. (single segment)order.#— matchesorder.created,order.payment.received, etc. (multi-segment)#— matches everything
Horizontal Scaling¶
Add more WebMQ backend instances, and your frontends distribute across them. RabbitMQ handles message routing regardless of which backend a client is connected to.
The highlighted path shows a message from App 2 reaching App 9 — they're on different backend instances, but RabbitMQ routes it through:
graph TB
subgraph "Web Apps"
App1[App 1]
App2[App 2]
App3[App 3]
App4[App 4]
App5[App 5]
App6[App 6]
App7[App 7]
App8[App 8]
App9[App 9]
App10[App 10]
end
subgraph "WebMQ Backend Instances"
Backend1[Backend 1<br/>ws://host1:8080]
Backend2[Backend 2<br/>ws://host2:8080]
end
RMQ[(RabbitMQ<br/>topic exchange)]
App1 --> Backend1
App2 -->|① publish| Backend1
App3 --> Backend1
App4 --> Backend1
App5 --> Backend1
App6 --> Backend2
App7 --> Backend2
App8 --> Backend2
App9 --> Backend2
App10 --> Backend2
Backend1 -->|② forward| RMQ
RMQ -.->|③ route| Backend2
Backend2 -.->|④ deliver| App9
linkStyle 1,10 stroke:#4caf50,stroke-width:3px;
linkStyle 11,12 stroke:#4caf50,stroke-width:3px,stroke-dasharray:5 5;
① App2 publishes a message → ② Backend1 forwards to RabbitMQ → ③ RabbitMQ routes to subscribers on Backend2 → ④ Backend2 delivers to App9.
A message published by any frontend reaches any other subscribed frontend or backend service — no matter which WebMQ instance they're connected to. To scale, add more backend instances behind a load balancer.
Fan-Out to Workers¶
A single frontend request can trigger work across multiple independent workers, each in a different language. Workers connect directly to RabbitMQ using AMQP — they don't need WebMQ at all.
graph LR
App[Web App] -->|search.request| Backend[WebMQ Backend]
Backend --> RMQ[(RabbitMQ<br/>city_search exchange)]
RMQ --> W1[Weather Worker<br/>Node.js]
RMQ --> W2[Wikipedia Worker<br/>Node.js]
RMQ --> W3[Images Worker<br/>Node.js]
RMQ --> W4[Attractions Worker<br/>Python]
RMQ --> W5[Custom Worker<br/>Go / Java / Ruby]
W1 -->|search.results| RMQ
W2 -->|search.results| RMQ
W3 -->|search.results| RMQ
W4 -->|search.results| RMQ
W5 -->|search.results| RMQ
RMQ --> Backend
Backend --> App
The frontend publishes a single search.request event. RabbitMQ fans it out to every worker bound to that pattern. Each worker processes independently (weather API, Wikipedia, image search, ...) and publishes results back. The frontend receives results as they arrive — real-time, parallel, multi-source.
This is shown in action in the city-search example and detailed in the cross-language workers guide.
Bidirectional Flow¶
Unlike traditional WebSocket setups where only the server pushes to clients, WebMQ lets frontends publish and subscribe equally:
- Publish: Frontend → Backend → RabbitMQ → Backend Services + Other Frontends
- Subscribe: Backend Services → RabbitMQ → Backend → Frontend
This symmetry means a React component can publish a "task" event that gets picked up by a Python worker, whose result is routed back to the same or different frontend.
Session Queues¶
Each WebSocket connection gets a dedicated auto-delete queue on RabbitMQ, named after the sessionId. This queue:
- Receives messages routed to topics the client subscribed to
- Persists messages while the client is disconnected (if queue is still alive)
- Is deleted when the client disconnects normally
- Survives abnormal disconnects and backend shutdown until the
queueTimeoutTTL expires (default 5 min), letting clients reconnect to any backend and resume without message loss - Supports session dedup: if a client reconnects with the same
sessionId, the old consumer is cancelled and replaced
Message Flow Detail¶
sequenceDiagram
participant F as Frontend
participant B as WebMQ Backend
participant R as RabbitMQ
Note over F,B: WebSocket opens
F->>B: identify(sessionId)
B->>R: assertQueue(sessionId)
B->>R: consume(sessionQueue)
R-->>B: consumerTag
B-->>F: ack(identify)
F->>B: listen('orders.*')
B->>R: bindQueue(sessionQueue, 'orders.*')
R-->>B: bound
B-->>F: ack(listen)
Note over F,R: Later, another service publishes
R-->>B: deliver to sessionQueue
B-->>F: message('order.created', payload)
F->>B: ack(messageId)
B->>R: ack(rmqMessage)
Note over F,R: Or the frontend publishes
F->>B: publish('order.created', payload)
B->>R: publish to exchange
Note over R: route to all bound queues
B-->>F: ack(publish)
- Identify: Frontend sends its
sessionId. Backend creates/asserts a RabbitMQ queue for that session and starts consuming from it - Listen: Frontend subscribes to a topic pattern. Backend binds the session queue to the exchange with that pattern
- Receive (async): RabbitMQ delivers messages matching the binding to the session queue. Backend forwards them over the WebSocket and waits for the client to ACK before acknowledging to RabbitMQ
- Publish: Frontend publishes to a routing key. Backend publishes the message to the exchange. RabbitMQ routes it to all matching queues (other clients, workers, backend services)