Skip to content

Performance

Toise is a single Go binary with an embedded event log (Pebble) and an in-memory projection. This page summarises what to expect and how to measure it for yourself.

Numbers are directional

The figures below were measured on Apple Silicon (arm64) developer hardware, where fsync is slower and more variable than the Linux x86_64 reference profile (8 cores / 16 GB). Treat them as directional and re-measure on your own hardware. Run the suite with make bench.

The shape of it

  • Reads are O(1)/O(n) over an in-memory snapshot, independent of total graph size. GraphQL, MCP, and the debug UI all sit on this one projection, so they share the same latency picture. With ~1.3k entities and ~1.3k relations in the projection, a full host detail page (identity + attributes + neighbours + history) renders in under 1 ms.
  • Ingestion is bounded by the durable append. The OTLP boundary batches a whole export's records into one Sync'd batch append, rather than one fsync per record. That is the single biggest ingestion lever.

Measured highlights

What Measurement
OTLP LogRecord convert + dispatch ~90 ns/op (excludes gRPC transport)
Event-log append (batch of 100, Sync) ~4.1 ms per batch
find_entities filtering 200 of 10k hosts ~1.86 ms/op
get_neighbors depth 2 ~330 ns/op
Projection rebuild (replay) ~440 ns/event (~0.44 s extrapolated for 1M events)

Batched append in practice

Ingesting a generated fabric over real OTLP/gRPC with toise-probe:

Fabric Entities Relations Per-record fsync Batched append (1 fsync/export)
120 hosts 633 639 ~5.4 s ~0.40 s (~13×)
250 hosts 1319 1333 ~8 s ~0.03 s (re-assert)

On the durable-append path alone, batching 100 entities into one Sync'd append is roughly 84× faster than 100 individual Sync'd appends.

Measure it yourself

make bench                                  # the microbenchmark suite
./bin/toise-server --data-dir ./live-data &
./bin/toise-probe --hosts 250 --interval 60s --heartbeat 6s

Then watch ingest latency and query a host detail via GraphQL or MCP.

Keeping it fast

  • Batch your exports on the producer side — one larger export beats many tiny ones, because durability cost is per-append, not per-record.
  • Page your queries — keep GraphQL first: modest and use cursors; stay under the complexity and timeout guardrails.
  • Set retention — cap log growth with retention_max_age; see Storage sizing.