34. Binary CBOR encoding for the client API
Status
Accepted
Context
-
Following ADR-9, the client API (WebSocket and HTTP) of the
hydra-nodeexchanges JSON messages exclusively. -
During high-throughput operation (notably the Hydra Doom event, see #1585), the JSON encoding proved to be a significant overhead: transactions of ~480 bytes inflate to several kilobytes because they are hex-encoded inside a JSON text envelope. #2543 suggested a binary protocol option over the WebSocket for the same reason.
-
Baseline measurements (criterion micro-benchmarks over the API hot-path messages) show a
SnapshotConfirmedover a 1000-UTxO head weighs ~446 KB of JSON, costing ~20 ms to encode on the node and ~48 ms to decode in the client, per message and per connected client. The?snapshot-utxo=nofilter even increases server-side cost because it rewrites already-encoded JSON bytes. -
The network layer already serializes messages with CBOR via the
cardano-binaryToCBOR/FromCBORclasses (seeHydra.Network.Message), establishing a codec convention: a leading text tag naming the constructor, followed by the constructor fields in declaration order. Node-to-node traffic is therefore already binary; the client API is the only JSON wire surface. -
gRPC/protobuf was considered and rejected: the dominant payloads are Cardano ledger types whose canonical serialization is CBOR, so protobuf messages would either wrap opaque CBOR blobs (no payload-level schema benefit) or require modeling the entire ledger in proto. gRPC would furthermore replace the WebSocket API instead of extending it, breaking browser-based clients.
Decision
-
All client API message types (and the protocol types they embed) get native, hand-written
ToCBOR/FromCBORinstances following the existingHydra.Network.Messageconvention. Text tags are identical to the JSONtagvalues, so both encodings share one schema vocabulary.- Types with derived fields (e.g. the
Snapshotaccumulator,SeenSnapshotsignable bytes) mirror their JSON instances: derived data is not transmitted and gets reconstructed on decode.
- Types with derived fields (e.g. the
-
The client API encoding is negotiated per connection, JSON remains the default and no node-wide flag exists:
- WebSocket clients opt in with the
encoding=cborquery parameter; messages are then exchanged as binary frames containing one CBOR term (Cardano.Binary.serialize'/decodeFull'). The server resolves the negotiated encoding once per connection into aWsCodecand dispatches on that, never on the frame type. - HTTP clients negotiate per request with
Content-Type: application/cbor(request bodies) andAccept: application/cbor(responses).
- WebSocket clients opt in with the
-
For CBOR connections, the
?snapshot-utxo=nofilter is applied as a typed transformation before encoding instead of the byte-level rewriting used for JSON: the snapshotutxois sent as an empty set rather than omitted. -
A CI-enforced property suite (
Hydra.CBORSpec) roundtrips every codec, keeping the hand-written encoder/decoder pairs in sync as types evolve.
Consequences
-
High-throughput deployments can shrink their wire footprint substantially: transaction payloads travel as raw CBOR bytes instead of hex text inside JSON, and JSON syntax overhead disappears (see
benchmarkson #2543 for before/after numbers). -
Existing clients (JSON) are entirely unaffected; hydra-cluster tests, the bench (
--cbor) and hydra-tui (--cbor) can exercise the binary path. -
Every new constructor in an API type now needs a CBOR codec next to its JSON instance; the roundtrip property suite fails CI when one is missing or inconsistent.
-
Event persistence is unchanged (JSON in SQLite), and the log stream remains JSON; a binary log format can be revisited separately.


