Skip to content

The wire protocol

Everything that talks to bisond — bisonsh, bisonc, Prairie, your code — uses one protocol: length-prefixed BSON documents over TCP, strictly one request then one response. It is small enough to implement a client in an afternoon; the engine repo's docs/protocol.md includes a from-scratch Python example.

Authenticated (v2), but not encrypted

Since wire protocol v2, a connection must authenticate (authenticate / authenticateToken) before any data command. There is still no TLS — the transport is clear text. See the Security page for the auth model, roles, and bootstrap flow; this page covers the framing and command set.

Framing

┌──────────────────┬──────────────────────────────┐
│ payloadLen (u32  │ payload: exactly one BSON    │
│ little-endian)   │ document, payloadLen bytes   │
└──────────────────┴──────────────────────────────┘
  • Valid payload range: 5 bytes to 16 MiB. A violating length means the byte stream is unrecoverable: the server sends a best-effort error frame and closes.
  • A well-framed but malformed BSON payload is recoverable — the stream is still in sync, so the server answers with ParseError/CorruptData and keeps serving.
  • No pipelining. One outstanding request per connection; open more connections for parallelism. Default port 27027.

Requests are {cmd: "<name>", ...args}. Success: {ok: true, ...}. Failure: {ok: false, error: {code, message}}.

Error codes

CodeMeaning
BadRequestmissing/mistyped argument, invalid collection name, off-loopback shutdown
UnknownCommandunrecognized cmd
ParseErrormalformed JSON/extended-JSON content
CorruptDatamalformed BSON payload or unreadable stored data
DuplicateKey_id already exists
NotFoundreferenced item absent
TooLargeframe or key size cap exceeded
ServerBusyconnection limit reached (sent on accept, then close)
Internalanything unexpected — the catch-all that keeps workers alive

Command catalog

Collection names match [A-Za-z0-9_][A-Za-z0-9_-]{0,127}.

CommandRequest argsSuccess payload
ping{}
serverStatusname, version, protocolVersion: 2, security: { auth, tls:false, setupMode }; authenticated callers also get uptimeSec, connectionsCurrent, opCounters
listCollectionscollections: [string]
createCollectioncollcreated: bool (false = existed)
dropCollectioncolldropped: bool
dbStatscollections: [{name, count, fileSizeBytes, indexes}]
insertcoll, documents: [...]insertedIds: [ObjectId], insertedCount
findcoll, filter, limit?, skip?documents, count (+ truncation, below)
deleteManycoll, filterdeletedCount
updateOnecoll, filter, update: {$set: {...}}matched, modified
createIndexcoll, fieldbuilt, docsIndexed, docsSkipped
dropIndexcoll, field{}
listIndexescollindexes: [string]
explaincoll, filter, limit?plan: {plan, index?, docsExamined, docsReturned}
compactcollstats: {documents}
shutdown{}, then graceful stop (loopback peers only)

A worked example

Request (shown as JSON; it travels as BSON):

json
{ "cmd": "find", "coll": "zips", "filter": { "pop": { "$gte": 40000 } }, "limit": 2 }

Response:

json
{
  "ok": true,
  "documents": [
    { "_id": {"$oid": "5c8eccc1caa187d17ca6ed16"}, "city": "ALPINE", "pop": 40912, ... },
    { "_id": {"$oid": "5c8eccc1caa187d17ca6ed99"}, "city": "BESSEMER", "pop": 45481, ... }
  ],
  "count": 2
}

find truncation — cursors without cursors

A response must fit one 16 MiB frame. When results don't fit, the server returns what does, plus:

json
{ "ok": true, "documents": [...], "count": 812, "truncated": true, "skipNext": 812 }

The client re-issues the same filter with skip = skipNext until truncated disappears. This is a deliberate simplification over server-side cursors: it is stateless on the server, trivially correct to implement, and its one weakness — results can shift if the collection mutates between batches — is acceptable for this database's scope. Both bundled clients (C++ and Rust) reassemble transparently.

Versioning

serverStatus.protocolVersion is 2 (v2 added the authentication handshake). Clients should check it on connect — Prairie blocks its workspace with an explanation when the number doesn't match, rather than failing on a later command. Pre-1.0 servers don't report the field at all (treat as 0); v1 servers report 1 and have no auth.

BisonDB and Prairie are GPLv3 · educational projects.