Docs
View as markdown

Size a store and its memory

Estimate the memory a relate graph needs from its counts, measure a real store's logical size, and cap growth with --logical-graph-size-limit.

cli

Before you start: Install relate

This guide shows you how to estimate the memory a store needs, measure the size of a real one, and cap its growth. relate holds the whole graph in memory: opening a store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary loads its last snapshot The committed graph state a checkpoint writes to disk, loaded on open before the WAL replays; not the snapshot durability mode or --read-only freshness. Read more in the glossary and replays the WAL Append-only log each commit goes to before it applies; under the default full durability mode it is synced first, so a crash loses no acknowledged write. Read more in the glossary into RAM, and there is no buffer pool or spill to disk. The graph you query has to fit in the process's memory, with room for the statements that run on it.

Prerequisites

  • relate on your PATH (Install relate).
  • Either the node, relationship, and property counts you expect, or a store to measure.

Estimate from counts

relate counts the size of a graph in logical bytes. The logical graph size relate's own byte count of the live nodes, relationships, properties and index entries; what --logical-graph-size-limit caps. Read more in the glossary is its own tally of the live nodes, relationships, properties, and index entries. That tally is what --logical-graph-size-limit compares against. It leaves out the WAL buffers, statement working memory and allocator overhead, which the next section adds back.

These figures were measured with the relate version this page is verified against: 10,000 elements of one shape written into a fresh .rl store, the store's logical size read with the method in Measure a real store, and the difference divided by 10,000.

ElementLogical bytes eachShape measured
A node with one label212(:Item)
Each further label on a node45(:Item:Thing)
Integer or float properties on one node64 for 1, 128 for 2, 244 for 3, 256 for 4, 476 for 5, 512 for 8{a: i, b: i, …}, all integers; one float measured 64
A string property, instead of an integer8 more, plus the string's length in UTF-8 bytes8 and 1,000 ASCII characters; 8 two-byte characters
A boolean property57{b: true}
A list of 384 floats, such as an embedding3,147 (an average over 100 nodes){v: [x IN range(1, 384) | toFloat(x)]}
A relationship with no properties444 at 1 relationship per node, 344 at 5, 320 at 20one type, (a)-[:LINKS]->(b)
A relationship property (integer)60{w: j}
An entry in a range index on an integer property61 to 64CREATE INDEX … ON (n.i)

Property costs grow in steps, not one property at a time, so a node with 5 properties costs nearly as much as one with 8.

Work an example

Take a memory graph for an agent: 1,000,000 :Memory nodes with an integer id, a 20-character name and an 8-character kind, 5,000,000 RELATES_TO relationships (5 out of each node) with an integer since, and a range index on id.

PartCountBytes eachLogical bytes
Nodes: label, 3 properties, 28 bytes of strings (measured; the per-element table gives 500)1,000,000502502,000,000
Relationships at 5 per node, 1 integer property5,000,0004042,020,000,000
Index entries on id1,000,0006464,000,000
Total2,586,000,000

A one-hundredth model of the same graph (10,000 nodes, 50,000 relationships, the same index) measured 25,901,217 logical bytes, which scales to 2,590,121,700: the estimate is within 0.2% of it.

Allow for process memory

The process needs more memory than the logical size, and the most while a store opens. Two measurements of this release:

MeasurementProcess memory for the graph
Once loaded: a served process after writes filled four graph shapes of about 33 MB logical each (Linux x86_64 in a 2 GB, 2-CPU container)1.22 to 1.54 times the logical size
Peak while opening a .rl store and decoding its snapshot: stores of 10,000 to 1,000,000 nodes on macOS arm64, median of 3 runs2.5 to 3.9 times the logical size; 2.5 at 1,000,000 nodes

For the worked example (2,586,000,000 logical bytes), that is about 3.2 to 4.0 GB once loaded, and about 6.5 GB while the store opens, at the 2.5 ratio of the largest measured store (rounded to 0.1 GB). The open is the peak, so size the machine from it. Statements need working memory on top: one value a statement builds, such as a list, may reach 256 MiB under the default --expression-memory-limit.

The size on disk is not a guide to memory: a 1,000,000-node .rl store of 187,416,576 bytes, measured with this release, holds 1,390,890,109 logical bytes.

Measure a real store

relate info reads a store's metadata without opening it, including the node and relationship counts at its last checkpoint The step that writes a fresh snapshot of committed state and recycles old WAL segments: every 300 seconds by default, on clean exit, and on .checkpoint. Read more in the glossary. To follow along, create the shared example graph first:

cat > team.cypher <<'EOF'
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
EOF
relate query --database ./team.rl < team.cypher

Read its counts:

relate info --database ./team.rl
layout: .rl bundle
superblock version: 1
codec: parquet
format version: 3
checkpoint LSN: 1
commit timestamp: 1
total bytes: 36864
wal segments past checkpoint: 1
nodes (at checkpoint LSN 1): 5
relationships (at checkpoint LSN 1): 5
indexes: 0
constraints: 0

The counts go straight into the estimate. To read the logical size itself, try a write against a size limit of 1 byte: the refusal reports the live size as used. --load with a new --database path does it on a copy, probe.rl, so the original store is never opened for writing:

relate --load ./team.rl --database ./probe.rl --logical-graph-size-limit 1 --mode envelope "CREATE (:Probe)"

The first two lines are stderr, the last is the error object on stdout:

--load ./team.rl: loaded into ./probe.rl
capacity exceeded: used=6555 limit=1
{"error":{"code":"fatal_error","exit_code":1,"kind":"capacity","message":"capacity exceeded: used=6555 limit=1","retryable":false}}

The example graph is 6,403 logical bytes. Remove the copy when you're done:

rm ./probe.rl

Cap the graph's size

--logical-graph-size-limit SIZE caps the logical size. It takes bytes, or a number with mb or gb (powers of 1000) or mib or gib (powers of 1024). The default is unlimited, and 0 means unlimited too. RELATE_LOGICAL_GRAPH_SIZE_LIMIT sets it for every run in an environment, and relate serve takes the same flag.

Set the cap below the memory you can give the process, divided by the open-peak ratio from Allow for process memory. A write that would grow the graph past the cap then fails with an error you can read, instead of the operating system ending the process.

This statement tries to add a node to team.rl under a cap of 6,500 bytes, which the 6,403-byte graph leaves no room for:

relate --database ./team.rl --logical-graph-size-limit 6500 --mode envelope "CREATE (:Person {name: 'Ines', role: 'engineer'})"
echo "exit=$?"
capacity exceeded: used=6555 limit=6500
{"error":{"code":"fatal_error","exit_code":1,"kind":"capacity","message":"capacity exceeded: used=6555 limit=6500","retryable":false}}
exit=1

What the cap does when a write reaches it:

SituationResult
A write whose counted change would take the graph past the capFails before it reaches the WAL, with exit code 1, kind capacity and retryable: false; nothing is written. A SET counts as a change even when the new value is the same size, so it needs room under the cap
used and limit in the messageThe logical size the write was checked at, and the cap in bytes. For a CREATE, used is the live size, which Measure a real store relies on; for a SET it is higher than the live size (101 bytes higher in the runs for this page)
A readRuns; reads are never checked
DELETERuns, and frees logical bytes at once
Opening a store already over the capLoads the whole graph. Until deletes free enough room, every write except a DELETE fails, even a SET or REMOVE that doesn't grow the graph

Verify

Read the store under the same cap, read-only. The read runs, and the refused write left the 3 people as they were:

relate --database ./team.rl --read-only --logical-graph-size-limit 6500 --mode envelope "MATCH (p:Person) RETURN count(p) AS people"
{"columnTypes":["INTEGER"],"columns":["people"],"notifications":[],"planCache":"miss","rows":[{"people":3}],"timings":{"elapsed_ms":0.34058299999999997,"execute_ms":0.164833,"lex_parse_ms":0.029375,"lower_ms":0.042624999999999996,"parse_ms":0.112292,"plan_ms":0.05450000000000001,"result_generation_ms":0.000292,"rewrite_ms":0.008417},"truncated":false}

If it goes wrong

  • capacity exceeded: used=… limit=…, exit code 1. A write reached --logical-graph-size-limit. Raise the cap if the machine has the memory, delete data you no longer need, or split the graph across stores. The errors reference has the entry.
  • This statement tried to commit a single WAL record of at least <used>. One commit's WAL record would be over the 64 MiB per-record cap, which is separate from the size limit. Commit the data in smaller transactions, such as batches of a bulk load (WAL payload cap).
  • Error: invalid --logical-graph-size-limit: semantic error: invalid memory limit unit 'kb'; use bytes, mb, mib, gb, or gib, exit code 2. The size has a unit relate doesn't accept. Write it in bytes or with one of the four units.
  • The process ends with no relate error while the graph grows. The operating system stopped it for memory. Under the default full durability mode, every write it acknowledged is in the WAL and comes back on the next open (Recover from a crash). Size the machine from the open peak, and set a cap below it.