ZigBolt: Perché Abbiamo Costruito il Nostro Aeron in Zig e Raggiunto 20 Nanosecondi per Messaggio
Ring buffer lock-free, codec zero-copy, cluster Raft — tutto in Zig puro, tutto open source.
Se lavori nel trading algoritmico o nel market making, conosci il prezzo di ogni microsecondo. Un context switch in più — e il tuo ordine arriva secondo. Una pausa GC della JVM — e il market maker dall'altra parte ha già aggiornato la quotazione. In un mondo dove il denaro si misura in nanosecondi, l'infrastruttura di messaggistica non è una noiosa tubatura tra servizi — è un vantaggio competitivo.
Abbiamo costruito ZigBolt — un sistema di messaggistica per il trading ad alta frequenza scritto in Zig. Da zero. Niente JVM, niente garbage collector, niente Media Driver, niente configurazioni XML. E abbiamo ottenuto 20 nanosecondi di latenza p50 su un ring buffer SPSC e 30 nanosecondi su IPC tramite memoria condivisa.
Questo articolo spiega perché ne avevamo bisogno, come funziona internamente e perché Zig.
TL;DR
- ZigBolt — sistema di messaggistica open-source (MIT) per HFT in Zig puro
- 20 ns p50 su SPSC, 30 ns p50 su IPC — più veloce dei numeri pubblicati da Aeron
- Codec zero-copy funziona a 0 ns (generazione a compile-time, a runtime è solo un cast di puntatore)
- Niente GC, niente JVM, niente Media Driver — la libreria si integra direttamente nella tua applicazione
- Cluster Raft, archivio, sequencer — tutto incluso
- Binding FFI per Rust, Python, Go, TypeScript, C — lavora nel linguaggio che preferisci
Il Problema: Perché Aeron È Ottimo Ma Non Sufficiente
Aeron di Real Logic è lo standard de facto per la messaggistica a bassa latenza nei mercati finanziari. Decine di aziende HFT lo usano, è battle-tested e ha un'architettura eccellente. Ma Aeron ha un problema fondamentale, e si chiama JVM.
JVM Safepoint: Il Nemico Invisibile
Anche se metti con cura tutti i dati nella memoria off-heap, anche se hai disabilitato l'ergonomia del GC e impostato GuaranteedSafepointInterval=300000 — la JVM si ferma comunque occasionalmente in un safepoint. Non è un bug, è una decisione architetturale: la JVM ha bisogno dei safepoint per la deottimizzazione, il biased locking e lo stack walking.
In pratica si presenta così: il tuo thread invia messaggi a p50 = 200 ns, e improvvisamente p99.9 sale a 50 µs. Senza motivo apparente. Perché uno dei thread della JVM ha deciso che era ora.
Media Driver: Un Hop in Più
Aeron funziona attraverso un Media Driver — un processo separato (o JVM embedded) che instrada i messaggi tra publisher e subscriber tramite memoria condivisa. Questo offre un buon isolamento ma aggiunge almeno un hop extra:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
Ogni hop significa nanosecondi in più, cache miss in più, imprevedibilità in più.
SBE: Un Passo di Build Separato
Simple Binary Encoding — il codec FIX standard per i messaggi finanziari. Nell'ecosistema Aeron, è un'utility Java separata che genera codice da schemi XML. Una dipendenza separata, un passo di build separato, un insieme di problemi separato.
La Soluzione: ZigBolt
Ci siamo chiesti: e se prendessimo le migliori idee di Aeron — log triple-buffered, ring buffer lock-free, cluster Raft — e le implementassimo in un linguaggio che:
- Non ha overhead di runtime (niente GC, niente safepoint)
- Permette la generazione di codice a compile time (comptime)
- Si integra banalmente con le librerie C (DPDK, io_uring)
- Compila in un binario di ~100 KB
Quel linguaggio è Zig.
Architettura
┌─────────────────────────────────────────────────────────┐
│ Publisher/Subscriber API (typed generic wrappers) │
├─────────────────────────────────────────────────────────┤
│ Transport Layer (channel factory & lifecycle) │
├─────────────────────────────────────────────────────────┤
│ IPC Channel (shared memory) │ UDP Channel (network) │
├─────────────────────────────────────────────────────────┤
│ WireCodec (comptime, zero-copy) │ SBE Encoder/Decoder │
├─────────────────────────────────────────────────────────┤
│ Ring Buffers (SPSC/MPSC) │ LogBuffer (triple-buffered) │
├─────────────────────────────────────────────────────────┤
│ Archive (replay) │ Sequencer (total order) │ Raft (HA) │
└─────────────────────────────────────────────────────────┘
Sette livelli, ognuno utilizzabile indipendentemente. Hai bisogno solo di un ring buffer SPSC per IPC tra due processi? Prendilo. Hai bisogno di un cluster completo con consenso Raft e archiviazione? C'è anche quello.
Benchmark: Numeri, Non Parole

Risultati reali dei benchmark su 10 milioni di iterazioni (Apple Silicon / macOS):
Ring Buffer SPSC
| Dimensione Messaggio | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 8 byte | 20 ns | 30 ns | 120 ns | 42,8M msg/s |
| 32 byte | 30 ns | 50 ns | 150 ns | 28,5M msg/s |
| 64 byte | 50 ns | 60 ns | 320 ns | 17,6M msg/s |
| 256 byte | 30 ns | 50 ns | 50 ns | 29,5M msg/s |
Canale IPC (memoria condivisa)
| Dimensione Messaggio | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 64 byte | 30 ns | 40 ns | 40 ns | 35,7M msg/s |
| 256 byte | 40 ns | 40 ns | 170 ns | 27,4M msg/s |
| 1024 byte | 90 ns | 260 ns | 900 ns | 9,9M msg/s |
LogBuffer (triple-buffered stile Aeron)
| Dimensione Messaggio | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 32 byte | 30 ns | 40 ns | 320 ns | 33,6M msg/s |
| 64 byte | 30 ns | 30 ns | 160 ns | 38,0M msg/s |
| 256 byte | 30 ns | 40 ns | 60 ns | 31,1M msg/s |
WireCodec (zero-copy comptime)
| Operazione | Latenza | Throughput |
|---|---|---|
| Encode (32 byte) | 0 ns | memcpy inline |
| Decode (32 byte) | ~0,4 ns | 2,7 miliardi msg/s |
Sì, hai letto bene: la codifica richiede zero nanosecondi. Perché WireCodec(T) valida la struct a compile time e trasforma encode/decode in un semplice @memcpy o cast di puntatore. Overhead a runtime = zero.
Per confronto: Aeron dichiara un RTT IPC (round-trip) di ~250 ns. La nostra latenza one-way è 30 ns. Anche contando il round-trip, siamo 4 volte più veloci.
Come Funziona Internamente
SPSC Lock-Free: La Semplicità Come Virtù

Un ring buffer single-producer single-consumer è la struttura dati lock-free più semplice e veloce. Lo scrittore sposta head, il lettore sposta tail, nessun CAS necessario — bastano gli atomici acquire/release.
Il trucco chiave è il padding della cache line. Se head e tail si trovano nella stessa cache line, ogni aggiornamento di un contatore invalida la cache per l'altro core (false sharing). La soluzione:
// Head (posizione di scrittura) — sulla propria cache line
head: std.atomic.Value(usize) align(128) = .init(0),
// 128 byte di padding — isolamento garantito
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (posizione di lettura) — sulla propria cache line
tail: std.atomic.Value(usize) align(128) = .init(0),
128 byte, non 64 — perché su Apple Silicon (e molti chip ARM) il prefetcher hardware può lavorare con coppie di cache line. Andiamo sul sicuro.
WireCodec: Comptime Invece della Generazione di Codice

Nel mondo Java/C++, i codec binari richiedono un passaggio separato: scrivi uno schema, esegui un generatore di codice, ottieni il codice, compila. In Zig, tutto questo avviene a compile time:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// Encode — solo un memcpy di 32 byte. Si inlinea in 1-2 istruzioni.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Decode — cast di puntatore. Zero copie.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Il compilatore Zig valida a comptime:
- La struct è packed (nessun buco di padding)
- La dimensione è un multiplo di 8 byte (allineamento per SIMD)
- Tutti i campi sono tipi primitivi
Se qualcosa è sbagliato — errore di compilazione, non un'eccezione a runtime alle 3 di mattina in produzione.
IPC tramite Memoria Condivisa
Due processi mappano lo stesso file in /dev/shm. Il publisher scrive nel ring buffer, il subscriber legge. Niente socket, niente system call sull'hot path:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber (un altro processo)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
L'intero percorso da publish() all'invocazione di handler_fn nel subscriber — 30 nanosecondi per un messaggio di 64 byte.
Affidabilità Basata su NAK per UDP
Per il trasporto di rete, ZigBolt usa la ritrasmissione guidata dal ricevitore. Il ricevitore traccia i gap nei numeri di sequenza tramite bitmap e invia NAK (negative acknowledgement) al mittente. Più il controllo della congestione AIMD — slow start stile TCP e congestion avoidance — per evitare di saturare la rete.
Cluster Raft: Quando Hai Bisogno di Consistenza
Per i casi in cui perdere un messaggio è inaccettabile (ad es. un matching engine), ZigBolt include il consenso Raft completo:
- Elezione del leader con timeout configurabile (150-300 ms)
- Replicazione del log — il leader replica ogni messaggio ai follower
- Write-ahead log con validazione CRC32 e crash recovery
- Snapshot — in modo che il WAL non cresca all'infinito
Archivio: Registra e Riproduci
Tutti i messaggi possono essere registrati in un archivio su disco segmentato. Poi — riproduzione da qualsiasi posizione per tempo o numero di sequenza. Compressione integrata stile LZ4 senza dipendenze esterne. Indice sparso per una ricerca rapida all'interno dei segmenti.
Sequencer a Ordine Totale
Per il market making su più venue, è fondamentale che tutti gli eventi abbiano un ordine globale. Il sequencer prende N stream di input e li fonde in uno solo, assegnando numeri di sequenza monotonicamente crescenti. Ogni partecipante vede la stessa sequenza di eventi.
Perché Zig e Non Rust/C/C++?
Abbiamo scelto tra quattro candidati. Ecco un confronto onesto:
| Criterio | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / overhead di runtime | Nessuno | Nessuno | Nessuno | Safepoint JVM, GC |
| Generazione di codice comptime | Nativa | Macro/template | proc macro | Nessuna |
| Interop C (DPDK, io_uring) | Triviale @cImport |
Nativo | FFI/bindgen | Overhead JNI |
| SIMD | @Vector, built-in |
Intrinsics | packed_simd (instabile) | Hint di vettorizzazione |
| Cross-compilazione | Built-in | CMake inferno | cargo target | N/A |
| Tempo di build | Secondi | Minuti (C++) | Minuti | Secondi + avvio JVM |
| Flusso di controllo nascosto | Nessuno | Eccezioni, cast impliciti | Panic in unwrap |
Eccezioni |
Zig ci ha dato una combinazione unica: performance a livello C + sicurezza durante lo sviluppo + metaprogrammazione comptime (codec, lookup table, macchine a stati del protocollo — tutto generato a compile time) + integrazione banale con DPDK, liburing, ef_vi tramite @cImport.
E un binario Zig pesa ~100 KB. Contro i 20+ MB di una soluzione basata su JVM.
Binding: Lavora nel Tuo Linguaggio
ZigBolt compila in una shared library con C-ABI, e abbiamo binding pronti per cinque linguaggi:
TypeScript / Node.js
import { IpcChannel } from "@zigbolt/node";
const channel = IpcChannel.create({
name: "/my-market-data",
termLength: 1024 * 1024,
});
const msg = Buffer.from("BTC/USDT 42000.50", "utf-8");
channel.publish(msg, 1);
Rust
use zigbolt::IpcChannel;
let ch = IpcChannel::create("/my-channel", 64 * 1024).unwrap();
ch.publish(b"hello", 1).unwrap();
let sub = IpcChannel::open("/my-channel", 64 * 1024).unwrap();
sub.poll(|data, msg_type_id| {
println!("got {} bytes, type={}", data.len(), msg_type_id);
}, 10);
Python
from zigbolt import IpcChannel
ch = IpcChannel.create("/market-data", term_length=1024*1024)
ch.publish(b"tick data here", msg_type_id=1)
Più Go e C standard. Lo stesso canale di memoria condivisa è accessibile da tutti i linguaggi simultaneamente — publisher in Zig, subscriber in Python, monitoraggio in Go. Leggono tutti la stessa regione mmap.
Codec SBE: Messaggi Compatibili FIX
Per i protocolli finanziari, ZigBolt include un codec SBE (Simple Binary Encoding) completo con schemi a compile time. Tipi di messaggi built-in:
- NewOrderSingle — invio ordine
- ExecutionReport — rapporto di esecuzione
- MarketDataIncrementalRefresh — aggiornamento incrementale dei dati di mercato
- MassQuote — quotazione di massa
- Heartbeat — verifica di connettività
- Logon — autenticazione
Nessun generatore di codice esterno, nessun XML. Tutto è descritto con struct Zig e validato a compile time.
Protocollo Wire: Compatibilità con Aeron
ZigBolt implementa i flyweight del protocollo wire compatibili con Aeron:
- DataHeaderFlyweight — frame di dati
- StatusMessage — controllo di flusso
- NAK — negative acknowledgement
- Setup, RTT, Error — frame di servizio
Questo significa che ZigBolt può coesistere con l'infrastruttura Aeron esistente. La migrazione non deve essere un big bang.
Cosa C'è Dopo
ZigBolt è attualmente alla versione 0.2.1. Il core è stabile, i benchmark sono riproducibili, i binding funzionano. In arrivo:
- Backend io_uring — trasporto di rete zero-copy su Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — kernel bypass per quando ogni microsecondo conta
- Multi-Raft — sharding per strumento/strategia
- Archivio colonnare — integrazione Apache Arrow/Parquet per l'analisi
- Supporto hugepage — hugepage 2MB/1GB pre-faulted per minimizzare i TLB miss
Provalo
- Sito web: zigbolt-landing.vercel.app
- Documentazione: zigbolt-landing.vercel.app/getting-started/introduction/
- Codice sorgente: github.com/suenot/zigbolt
- Licenza: MIT
Compila dai sorgenti (zig build), esegui i benchmark (zig build bench), connetti tramite FFI da qualsiasi linguaggio. Se hai Zig 0.15.1 e un paio di minuti — prova il benchmark ping-pong e confronta con la tua soluzione attuale.
Link:
- ZigBolt Landing: zigbolt-landing.vercel.app
- GitHub: github.com/suenot/zigbolt
- Aeron (per confronto): github.com/real-logic/aeron | la nostra panoramica di Aeron
- Linguaggio Zig: ziglang.org
- Marketmaker.cc: marketmaker.cc
Citazione
@software{soloviov2026zigbolt,
author = {Soloviov, Eugen},
title = {ZigBolt: Why We Built Our Own Aeron in Zig and Hit 20 Nanoseconds Per Message},
year = {2026},
url = {https://marketmaker.cc/it/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {Come e perché abbiamo costruito da zero un sistema di messaggistica a latenza ultra-bassa per HFT in Zig. Niente JVM, niente GC, niente sorprese.}
}
Autori
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.