Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The FireMQ protocol

Version 1, release candidate. The version freezes once the Rust, Go and TypeScript SDKs pass the conformance suite; from then on, changes are additive only and gated by capabilities.

The FireMQ protocol is the native protocol of the FireMQ broker. It carries the same model as AMQP 0-9-1 (exchanges, queues, bindings, acknowledgements, publisher confirms) in a format that is small enough to implement in an afternoon in any language with a MessagePack library.

FireMQ also speaks AMQP 0-9-1 on port 5672. Both protocols share one broker, so a message published over one is consumed over the other.

Design rules

  • One connection is one session. There are no channels.
  • Every frame has a MessagePack header and an optional raw payload. Message bodies are never MessagePack-encoded.
  • Requests carry a request id (rid) and get exactly one reply: ok, error, or an op-specific reply.
  • Omitted header fields take their zero value: false, 0, empty string, empty map. Implementations ignore header keys they do not know.
  • All integers in framing are big-endian.

Transports

TransportDefault addressFraming
TCPport 5673Client first sends the 4 ASCII bytes FMQ1, then length-prefixed frames.
TLSport 5674As TCP, inside TLS.
WebSocketws://host:15672/fireOne binary WebSocket message per frame, without the length prefix and without FMQ1.

Connection URLs use the schemes fire://, fires:// (TLS), ws:// and wss://:

fire://user:password@host:5673/vhost

For fire and fires, the vhost is the URL path with the leading / removed and percent-decoded; an empty path means vhost /. For ws and wss, the path is the WebSocket endpoint (/fire when empty) and the vhost is the vhost query parameter, for example ws://localhost:15672/fire?vhost=dev. The default user and password are guest and guest. The default ports are 5673 (fire), 5674 (fires), 15672 (ws) and 15671 (wss).

Frame format

On TCP and TLS:

+-----------+--------+------------+---------------------+------------------+
| len : u32 | op : u8| hlen : u32 | header (hlen bytes) | payload          |
+-----------+--------+------------+---------------------+------------------+
  • len counts every byte after the len field: 1 + 4 + hlen + payload length.
  • header is a MessagePack map with string keys. hlen may be 0, meaning an empty map.
  • payload is the rest of the frame. Only publish, return, deliver and get_ok carry a payload.

On WebSocket the frame is the same without len.

Before welcome, a frame may be at most 65 536 bytes. After welcome, the limit is welcome.max_frame (16 MiB by default). A message body must fit in one frame.

Op codes

OpNameDirectionPayload
0x01helloclient → server
0x02welcomeserver → client
0x03pingboth
0x04pongboth
0x05closeboth
0x06okserver → client
0x07errorserver → client
0x08flowserver → client
0x10declare_exchangeclient → server
0x11delete_exchangeclient → server
0x12declare_queueclient → server
0x13delete_queueclient → server
0x14purge_queueclient → server
0x15bindclient → server
0x16unbindclient → server
0x20publishclient → servermessage body
0x21confirmserver → client
0x22returnserver → clientmessage body
0x30subscribeclient → server
0x31unsubscribeclient → server
0x32creditclient → server
0x33deliverserver → clientmessage body
0x34cancelledserver → client
0x35ackclient → server
0x36nackclient → server
0x37getclient → server
0x38get_okserver → clientmessage body

A server that receives an unknown op replies error with code 540 and keeps the connection open.

Connection lifecycle

client                                   server
  | -- FMQ1 (TCP only) ------------------> |
  | -- hello ----------------------------> |
  | <-------------------- welcome or error |
  | ... requests, publishes, deliveries ...|
  | -- close ----------------------------> |
  | <------------------------------- close |

hello

FieldTypeMeaning
veruintProtocol major version. Must be 1.
vhoststrVirtual host. Empty means /.
authmap{"mech": "plain", "user": str, "pass": str}.
heartbeatuintDesired heartbeat interval in seconds. 0 disables heartbeats.
clientmapFree-form client info, for example {"name": "firemq-go", "version": "0.1.0", "lang": "go"}. Shown in the management UI.
caps[str]Capabilities the client supports.

welcome

FieldTypeMeaning
veruintProtocol version in use.
servermap{"name": "firemq", "version": str}.
conn_idstrConnection name, as shown in the management API.
heartbeatuintNegotiated heartbeat interval in seconds: the smaller non-zero value of the client's and the server's.
max_frameuintLargest frame the server accepts, in bytes.
caps[str]Capabilities the server supports. Version 1 servers send confirms, credit, mandatory, exchange-bindings, dead-letter and ttl.

If authentication fails, the server replies error with code 403 and fatal: true, then closes the socket.

Heartbeats

When the negotiated interval h is non-zero, each side sends ping when it has sent nothing for h / 2 seconds. Each side considers the peer dead when it has received nothing for 2 × h seconds. A peer that receives ping replies pong.

close

{"code": uint, "msg": str}. The receiver replies with its own close and closes the socket. Code 200 means a normal shutdown.

Replies

ok

FieldTypePresent for
riduintalways
namestrdeclare_queue (the queue's name, useful for server-named queues)
messagesuintdeclare_queue, delete_queue, purge_queue
consumersuintdeclare_queue
emptyboolget when the queue is empty

error

FieldTypeMeaning
riduintThe failed request, or absent for errors not tied to a request.
codeuintAn AMQP reply code (table below).
msgstrHuman-readable reason.
fatalbooltrue when the server closes the connection after this frame.
CodeNameTypical cause
320connection-forcedServer shutting down.
403access-refusedBad credentials, or no permission for the resource.
404not-foundQueue or exchange does not exist.
405resource-lockedExclusive queue owned by another connection.
406precondition-failedRedeclared with different properties, unknown delivery tag, if_unused or if_empty failed.
501frame-errorMalformed frame. Fatal.
502syntax-errorHeader is not a MessagePack map, or a field has the wrong type.
503command-invalidRequest not allowed in the current state, such as a request before hello.
506resource-errorServer out of a resource.
530not-allowedOperation not allowed, such as deleting a built-in exchange.
540not-implementedUnknown op or unsupported feature.
541internal-errorBug in the server.

flow

{"pause": bool}. The server sends pause: true when a memory or disk alarm fires. Clients stop publishing until pause: false. A client that keeps publishing is throttled by the server no longer reading its socket.

Topology

declare_exchange

FieldTypeMeaning
riduintRequest id.
namestrExchange name. Names starting with amq. are reserved.
typestrdirect, fanout, topic or headers.
durableboolSurvives a broker restart.
auto_deleteboolDeleted when its last binding is removed.
internalboolCannot be published to directly.
passiveboolOnly check that it exists.
argsmapExchange arguments, for example alternate-exchange.

Reply: ok. Redeclaring an existing exchange with different type, durable, auto_delete or internal fails with 406.

delete_exchange

{"rid", "name", "if_unused": bool}. Reply: ok.

declare_queue

FieldTypeMeaning
riduintRequest id.
namestrQueue name. Empty means the server picks a unique name starting with amq.gen-.
durableboolSurvives a broker restart.
exclusiveboolUsed only by this connection and deleted when it closes.
auto_deleteboolDeleted when its last subscription is removed.
passiveboolOnly check that it exists.
argsmapQueue arguments with RabbitMQ's names: x-message-ttl, x-expires, x-max-length, x-max-length-bytes, x-overflow, x-dead-letter-exchange, x-dead-letter-routing-key, x-single-active-consumer, x-max-priority.

Reply: ok with name, messages and consumers.

delete_queue

{"rid", "name", "if_unused": bool, "if_empty": bool}. Reply: ok with messages, the number of messages deleted.

purge_queue

{"rid", "name"}. Reply: ok with messages, the number of messages purged.

bind and unbind

FieldTypeMeaning
riduintRequest id.
exchangestrSource exchange.
queuestrDestination queue.
to_exchangestrDestination exchange, for exchange-to-exchange bindings. Exactly one of queue and to_exchange is set.
keystrBinding key.
argsmapBinding arguments, for example x-match for headers exchanges.

Reply: ok.

Publishing

publish

FieldTypeMeaning
sequintClient-assigned sequence number, starting at 1 and increasing by 1. 0 means no confirm is wanted.
exchangestrTarget exchange. Empty is the default exchange, which routes to the queue named by key.
keystrRouting key.
mandatoryboolReturn the message with return when no queue receives it.
persistentboolWrite the message to disk. Survives a restart when the queue is durable.
propsmapMessage properties (below).
headersmapApplication headers. Values are any MessagePack type.

The payload is the message body.

Message properties:

KeyTypeAMQP property
content_typestrcontent-type
content_encodingstrcontent-encoding
correlation_idstrcorrelation-id
reply_tostrreply-to
expirationuintexpiration, in milliseconds
message_idstrmessage-id
timestampuinttimestamp, in seconds since the Unix epoch
typestrtype
user_idstruser-id. When set, it must equal the connection's user.
app_idstrapp-id
priorityuintpriority, 0 to 255

confirm

The server confirms every publish with a non-zero seq exactly once.

  • {"upto": n} confirms every outstanding publish with seq <= n. The broker has taken responsibility for those messages: they are on disk when persistent and routed to a durable queue, and in memory otherwise.
  • {"seq": n, "nack": true, "code": uint, "msg": str} rejects one publish, for example because a queue with x-overflow: reject-publish is full. The message was not enqueued in every queue that should have received it.

A nack for a sequence number is always sent before any upto that covers it.

return

{"seq", "code", "msg", "exchange", "key", "props", "headers"} with the message body as payload. Sent for a mandatory publish that no queue received, with code 312 (no-route). The return is sent before the publish's confirm.

Consuming

subscribe

FieldTypeMeaning
riduintRequest id.
subuintClient-chosen subscription id, unique on the connection.
queuestrQueue to consume from.
credituintInitial credit: how many messages the server may send before the client grants more.
no_ackboolMessages count as acknowledged once sent.
exclusiveboolFail with 405 if the queue has other consumers, and block new ones.
tagstrConsumer tag shown in the management API. Defaults to fire-<sub>.
argsmapConsumer arguments.

Reply: ok.

credit

{"sub": uint, "credit": uint} adds credit to a subscription. The server sends at most as many deliver frames as the credit granted so far. Clients usually grant their buffer size at subscribe, then top up by half that size each time they have processed half of it.

deliver

FieldTypeMeaning
subuintSubscription id.
taguintDelivery tag, unique and increasing on the connection, starting at 1.
redeliveredboolThe message was delivered before and not acknowledged.
exchangestrExchange the message was published to.
keystrRouting key it was published with.
propsmapMessage properties.
headersmapApplication headers.

The payload is the message body.

ack and nack

  • ack: {"tag": uint, "multiple": bool}. With multiple, acknowledges every unacknowledged delivery with a tag up to and including tag.
  • nack: {"tag", "multiple", "requeue": bool}. Requeued messages are delivered again with redelivered: true. Messages that are not requeued are dead-lettered when the queue has a dead-letter exchange, and dropped otherwise.

Neither has a reply. An unknown tag produces a non-fatal error with code 406.

unsubscribe

{"rid", "sub"}. Reply: ok. The server sends no more deliveries for sub. Unacknowledged deliveries stay unacknowledged until the client acks them or the connection closes, when they are requeued.

cancelled

{"sub", "code", "msg"}. The server ended a subscription, for example because its queue was deleted.

get and get_ok

get is {"rid", "queue", "no_ack": bool}. When a message is ready, the server replies get_ok:

FieldTypeMeaning
riduintRequest id.
taguintDelivery tag for ack and nack.
redeliveredboolAs in deliver.
exchangestrAs in deliver.
keystrAs in deliver.
remaininguintMessages still ready in the queue.
propsmapMessage properties.
headersmapApplication headers.

When the queue is empty, the reply is ok with empty: true.

Interoperability with AMQP 0-9-1

FireMQAMQP 0-9-1
persistent: truedelivery-mode: 2
props.expiration (uint ms)expiration (decimal string)
MessagePack nil, bool, int, float, str, bin, array, mapfield table V, t, l, d, S, x, A, F
AMQP D (decimal) and T (timestamp)float and uint

Reserved for later versions

These names are reserved so later versions can add them without breaking version 1 clients:

  • Streams: declare_queue with args["x-queue-type"] = "stream", a subscribe field offset ("first", "last", "next", a uint, or {"ts": uint}), a deliver field offset, and an op 0x39 commit_offset.
  • Capabilities streams, zstd and scram.

Test vectors

test-vectors/frames.json lists one encoded frame per op with its decoded header and payload. Decoders must decode every vector to the listed header. Encoders do not have to reproduce the bytes exactly, because MessagePack allows several encodings of the same map.