Skip to content

Service Levels

What Crystal guarantees, what it does not, and what it has been measured doing. This page describes the engine's properties. An availability SLA for a deployment is a property of how you run it — how many replicas, on what hardware, behind what ingress — and this page is the input to that calculation rather than a substitute for it.

Figures below were measured on Go 1.26, linux/amd64, using the repository's own benchmark fixture.

Durability

Property Guarantee
Acknowledged write survives process kill Yes
Acknowledged write survives power loss Yes, subject to the disk honouring fsync
Acknowledged write survives loss of the volume No — replicate or back up
Partially applied batch after a crash Impossible
Silent corruption on load Impossible — refuses to start instead

A write is acknowledged only after its WAL record is written and fsynced. Snapshots are written to a temp file, fsynced, renamed, and the directory fsynced — because the WAL is truncated immediately after a successful snapshot, a snapshot that silently did not land would take the only other copy with it.

Batches are atomic across a crash as well as within the process: all of a transaction's records are written in one fsync and only the last carries a commit marker, so recovery discards any transaction whose marker never landed.

What is not durable: anything the client did not receive a success response for. A crash mid-write loses that write and only that write — recovery reports the discarded bytes rather than hiding them.

Consistency

Scope Level
Single operation Linearizable
Batch (Apply, POST /api/v1/batch) Atomic — all or nothing
Read inside View Snapshot isolation
Traversal, query, neighbour lookup Snapshot isolation (they run inside a view)
Read on the primary after a write on the primary Read-your-writes
Read on a follower after a write on the primary No guarantee — asynchronous

All writes serialise through a single lock, so there is one global order and no write-write conflicts to resolve. Reads inside a view see one instant of the graph: an operation assembled from several reads can never observe an edge whose endpoint was deleted between two of them.

Referential integrity is enforced on write — edge endpoints must exist, and vertex deletion cascades inside the same transaction. Data written by older builds is checked at startup and reported, not silently trusted.

What is not offered: interactive read-write transactions (Begin/Commit), multi-graph transactions, and any cross-node consistency. Replication is asynchronous with no consensus.

Availability

Crystal is a single process per graph. Its availability is what you build around it.

Failure Effect
Process restart Unavailable for the duration of startup, which is O(graph)
Storage volume lost Graph lost unless replicated or backed up
One graph unhealthy The whole node reports not-ready
Follower fails No effect on the primary
Primary fails Writes unavailable until an operator promotes a follower

Startup reads the snapshot, replays the WAL and rebuilds the adjacency index, so restart time grows with graph size. Bound WAL replay with max_wal_bytes; the snapshot load itself is proportional to the graph.

Readiness (/readyz) covers every graph the process serves, so one broken graph depools the instance — it cannot serve the routes addressing that graph, and there is no way to depool a subset of an instance's routes. Liveness (/livez) deliberately ignores storage, because restarting does not fix a full disk.

Failover is manual. See the cluster guide.

Capacity

The binding limit is RAM, not disk.

Marginal cost
Per vertex 745 bytes
Per edge 626 bytes
live heap ≈ 745 B × |V| + 626 B × |E|

Budget RAM at 2× that for garbage-collector headroom.

RAM Graph at benchmark density (|E| ≈ 10·|V|)
4 GB ~300k vertices / ~3M edges
16 GB ~1.2M vertices / ~12M edges
64 GB ~4.9M vertices / ~49M edges

Supported envelope: ~1M vertices / ~10M edges on a 16 GB host. Past that the engine runs but nothing is designed or benchmarked for it.

Property payload dominates these numbers, not graph structure — measure at your own density.

Exceeding RAM is currently unrecoverable

A graph that outgrows its host is OOM-killed, and because startup loads the whole graph, the restart is killed too. There is no degraded mode and no way back in through Crystal. Size with headroom and alert on /api/v1/diagnostics memory well before the limit. Fixing this is tracked as an epic (admission control, capacity checks at startup, and a maintenance mode that can export or prune without loading the graph).

Performance

Measured, single node, 100k vertices / 1M edges unless stated.

Operation Observed
Get vertex by ID ~1 µs
Get edges by vertex ~10 µs
BFS depth 3 ~2 ms
BFS depth 5 ~84 ms
Query by type, limit 100 ~10 ms
Property-filter query (full scan) ~65 ms
Read during a concurrent checkpoint (40k vertices) 7–39 ms worst case

Reads are map lookups; nothing touches disk on the read path. Writes cost one fsync each — that is the floor, and it is why batching several mutations into one Apply is dramatically cheaper than issuing them separately.

Known slow paths, all bounded but all O(N):

  • Queries filter by scanning. There are no property indexes yet, so a PropertyEquals over 100k vertices visits all of them.
  • GetVerticesByType / GetEdgesByType scan the full map.
  • Pagination bounds the response, not the scan.

Bounded by configuration, not by luck: traversal depth, path enumeration depth, result count and wall-clock timeout all have ceilings, and requests asking for more are clamped with the response flagged truncated.

Operational limits

Limit Default Setting
Request body 4 MiB server.max_body_bytes
Traversal depth 20 traversal.max_depth
Path enumeration depth 6 traversal.max_path_depth
Paths returned 1000 traversal.max_path_results
Path search wall clock 5s traversal.path_timeout
WAL size before checkpoint 64 MiB storage.max_wal_bytes
Checkpoint interval 30s storage.flush_interval
Graphs per process 32 storage.max_graphs
Graceful shutdown 15s server.shutdown_timeout

Security posture

  • Authentication is required by default; the server refuses to start with it enabled and no tokens.
  • Tokens carry read or write scope. Scope is per process, not per graph — one Crystal process is one trust boundary.
  • CORS is closed by default; a wildcard origin is rejected while auth is on.
  • TLS is supported natively or can be terminated upstream.
  • Health probes are public by design; runtime diagnostics are authenticated.

Recovery objectives

These follow from the above rather than being promises the engine makes on its own — they are what a deployment can achieve.

Objective Single node With a follower
RPO (data loss) 0 for acknowledged writes, unless the volume is lost replication lag, typically sub-second
RTO (time to serve) process restart + O(graph) load manual promotion, minutes

To achieve RPO 0 against volume loss you need synchronous replication, which Crystal does not offer. Back up graph.json — it is a plain, self-describing, versioned file, and a checkpoint makes it current.