Author: Giulio Rebuffo
Introduction
The SSZ Engine API spec proposes replacing JSON-RPC encoding with binary SSZ (Simple Serialize) for the Ethereum Engine API — the interface between consensus layer (CL) and execution layer (EL) clients.
Today, every message between CL and EL is JSON-encoded: every byte of every transaction, blob, and withdrawal is hex-encoded and wrapped in JSON. For small payloads like forkchoiceUpdated, this is fine. For engine_getPayloadV5 — which returns the full ExecutionPayload plus BlobsBundle — the encoding overhead becomes a meaningful contributor to block propagation latency.
Transport
Both transports coexist on the same Engine API port (default 8551):
| Transport | Content-Type | Endpoint |
|---|---|---|
| JSON-RPC | application/json |
POST / |
| SSZ REST | application/octet-stream |
POST /engine/v{N}/{resource} |
With PeerDAS and the Fulu fork pushing blob counts from 6 to 72 (target 48), payload sizes through the Engine API will grow significantly. This post presents encoding benchmarks from a live Kurtosis devnet running three EL implementations with SSZ transport instrumentation.
Methodology
Devnet Setup
A Kurtosis devnet was deployed with 12 EL+CL pairs — every combination of three EL clients (Geth, Erigon, Nethermind) and four CL clients (Prysm, Lighthouse, Teku, Lodestar) — all running custom Docker images with SSZ transport support and encoding instrumentation.
Network parameters: 6-second slots, Fulu fork at genesis, 72 max blobs, 48 target blobs, 32 validators per node. Transaction load generated with spamoor at 200 EOA tx/s plus blob transactions (6 sidecars each).
Measurement
Each EL was instrumented to encode every GetPayloadV5 response in both SSZ and JSON, logging the size and time for each:
GetPayloadV5 encoding: SSZ=9,930,470 bytes in 19,700 us | JSON=19,906,846 bytes in 59,531 us | ratio=2.0x size, 3.0x time blobs=72
Results
getPayload Encoding Time at 72 Blobs (Fulu Max)
Worst observed encoding times per EL+CL pair at 72 blobs (~9.9 MB SSZ / ~19.9 MB JSON) after 17 hours of continuous devnet operation:
| CL \ EL | Nethermind | Geth | Erigon |
|---|---|---|---|
| Prysm | SSZ 5 ms / JSON 103 ms (20.6×) | SSZ 19 ms / JSON 180 ms (9.3×) | SSZ 15 ms / JSON 63 ms (4.2×) |
| Lighthouse | SSZ 8 ms / JSON 114 ms (14.3×) | SSZ 24 ms / JSON 181 ms (7.5×) | SSZ 6 ms / JSON 82 ms (12.7×) |
| Teku | SSZ 14 ms / JSON 120 ms (8.6×) | SSZ 28 ms / JSON 358 ms (12.8×) | SSZ 13 ms / JSON 81 ms (6.1×) |
| Lodestar | SSZ 8 ms / JSON 91 ms (11.4×) | SSZ 36 ms / JSON 446 ms (12.3×) | SSZ 31 ms / JSON 101 ms (3.3×) |
These are worst-case measurements (highest observed encoding times) across a 17-hour run — real-world typical times are lower, but worst-case latency is what matters for block propagation deadlines.
Key observations:
- SSZ encoding stays under 36 ms across all 12 pairs in the worst case — consistently fast regardless of implementation.
- JSON worst-case encoding ranges from 63 ms to 446 ms — highly variable across EL implementations.
- Geth has the highest JSON overhead (180–446 ms), making the SSZ speedup most dramatic there (7.5–12.8×).
- Nethermind worst-case JSON encoding ranges from 91–120 ms, with SSZ 8.6–20.6× faster.
- Erigon sits in between (63–101 ms JSON), with 3.3–12.7× SSZ speedup.
Wire Size
Consistently across all blob-carrying payloads:
| Payload | SSZ Size | JSON Size | Ratio |
|---|---|---|---|
| 72-blob block | ~9.9 MB | ~19.9 MB | 2.0× |
| 24-blob block | ~3.3 MB | ~6.6 MB | 2.0× |
| 6-blob block | ~837 KB | ~1.68 MB | 2.0× |
The 2× ratio is structural: JSON hex-encoding doubles every byte (0xff → "0xff" = 4 chars).
Other Engine API Methods
Not all Engine API calls benefit equally from SSZ. The three methods called every slot are forkchoiceUpdated, newPayload, and getPayload. Here is how they compare in terms of request/response sizes and encoding times (all measurements from the Erigon + Prysm pair):
Message sizes:
| Method | Direction | SSZ Size | JSON Size |
|---|---|---|---|
forkchoiceUpdated |
CL → EL request | 100–200 B | 300–600 B |
forkchoiceUpdated |
EL → CL response | 49–57 B | ~200 B |
newPayload |
CL → EL request | 603 B – 46 KB | 1.4 KB – 92 KB |
getPayload |
EL → CL response | 837 KB – 9.9 MB | 1.68 MB – 19.9 MB |
Encoding times:
| Method | SSZ Time | JSON Time |
|---|---|---|
forkchoiceUpdated |
<1 µs | <1 µs |
newPayload |
15–45 µs | 60–4,978 µs |
getPayload |
1.1–25.6 ms | 16–211 ms |
forkchoiceUpdated is tiny in both directions — under 200 bytes. The overhead difference between SSZ and JSON is negligible at this scale.
newPayload carries the ExecutionPayload (transactions, withdrawals) but not blobs — those propagate via the gossip layer. Even on a high-throughput mainnet block (~1,500 transactions), newPayload would be roughly 200–400 KB in SSZ. At these sizes, the encoding overhead is well under 1 ms regardless of format.
getPayload is the outlier. It returns the full ExecutionPayload plus the BlobsBundle (commitments, proofs, and all blob data). At 72 blobs, this single response is 9.9 MB in SSZ (19.9 MB in JSON) — orders of magnitude larger than any other Engine API message. This is where encoding overhead becomes measurable in milliseconds, and why the SSZ transport spec focuses its impact here.
Conclusion
The Engine API’s getPayload response is the largest message exchanged between CL and EL every slot. At 72 blobs, it carries ~9.9 MB of data in SSZ (19.9 MB in JSON) and this single encoding step can take anywhere from 63 ms to 446 ms in JSON depending on the client, eating directly into the block propagation budget.
SSZ encoding of the same payload takes 5–36 ms across all 12 tested EL+CL pairs. The speedup ranges from 3× to 20× depending on the pair, with the worst-case SSZ time still well below the best-case JSON time for most implementations.
The wire size reduction is a consistent 2× across all payload sizes — a structural consequence of eliminating hex encoding. For a 72-blob block, that’s ~10 MB saved per slot on the CL↔EL link.
The cost of adoption is low: both transports coexist on the same port, JSON-RPC remains the default, and SSZ is opt-in via content-type negotiation. Implementations already exist for Geth, Erigon, Nethermind, Prysm, Lighthouse, Teku, and Lodestar.
Reproduction
Kurtosis devnet config
participants:
# 3 ELs × 4 CLs = 12 pairs
- el_type: geth
cl_type: prysm
supernode: true
validator_count: 32
- el_type: geth
cl_type: lodestar
supernode: true
validator_count: 32
- el_type: geth
cl_type: teku
supernode: true
validator_count: 32
- el_type: geth
cl_type: lighthouse
supernode: true
validator_count: 32
- el_type: erigon
cl_type: prysm
supernode: true
validator_count: 32
- el_type: erigon
cl_type: lodestar
supernode: true
validator_count: 32
- el_type: erigon
cl_type: teku
supernode: true
validator_count: 32
- el_type: erigon
cl_type: lighthouse
supernode: true
validator_count: 32
- el_type: nethermind
cl_type: prysm
supernode: true
validator_count: 32
- el_type: nethermind
cl_type: lodestar
supernode: true
validator_count: 32
- el_type: nethermind
cl_type: teku
supernode: true
validator_count: 32
- el_type: nethermind
cl_type: lighthouse
supernode: true
validator_count: 32
network_params:
seconds_per_slot: 6
fulu_fork_epoch: 0
bpo_1_max_blobs: 72
bpo_1_target_blobs: 48
additional_services:
- spamoor
spamoor_params:
spammers:
- scenario: eoatx
config:
throughput: 200
max_pending: 400
max_wallets: 200
- scenario: blob-combined
config:
throughput: 40
max_pending: 80
sidecars: 6
kurtosis run github.com/ethpandaops/ethereum-package --args-file ssz_bench_net.yml
Implementations
- Spec: feat: add ssz to engine api by barnabasbusa · Pull Request #764 · ethereum/execution-apis · GitHub
- Geth: eth/catalyst: add SSZ-encoded engine API by Giulio2002 · Pull Request #33926 · ethereum/go-ethereum · GitHub
- Erigon: SSZ-REST Engine API transport (EL server) by Giulio2002 · Pull Request #19670 · erigontech/erigon · GitHub
- Nethermind: feat: SSZ-REST Engine API transport by Giulio2002 · Pull Request #10728 · NethermindEth/nethermind · GitHub
- Prysm: SSZ-REST Engine API Transport for Prysm by Giulio2002 · Pull Request #16447 · OffchainLabs/prysm · GitHub
- Lighthouse: SSZ-REST Engine API transport (CL client) by Giulio2002 · Pull Request #8937 · sigp/lighthouse · GitHub
- Teku: SSZ-REST Engine API transport (CL client) by Giulio2002 · Pull Request #10455 · Consensys/teku · GitHub
- Lodestar: feat: SSZ-REST Engine API transport by Giulio2002 · Pull Request #8994 · ChainSafe/lodestar · GitHub
