An object store on your own disks: large values, striped reads, and where it stands
12 October 2026 · Semurg · 3 min read · Markdown
Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)
An object store keeps values you never look inside: images, files, model outputs, backups, under keys you choose. Semurg stores each value as one record that names it, followed by a run of fixed-size pieces spread evenly across every disk the node has, so reading a large value back is one sweep with every disk working at once.
Store a value
Values travel base64 encoded, under whole-number ids. This one is a sentence of a little over a hundred bytes, which is already long enough to be stored as a record plus a run of pieces:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op": "object_put",
"objects": [{"id": 900000000501,
"value_b64": "U2VtdXJnIG9iamVjdCBleGFtcGxlOiBhIHZhbHVlIGxvbmdlciB0aGFuIHR3ZW50eSBieXRlcyBpcyBhbiBhbmNob3IgcGx1cyBhIHJ1biBvZiBwaWVjZXMgc3RyaXBlZCBhY3Jvc3MgZXZlcnkgZGlzay4="}]}'Expected: the value is written
A value of twenty bytes or less lives inside its record. A longer one is laid down as its record, a short manifest and the pieces, each carrying forty-four bytes of the value, spread across the disks, and the record is written last, so a value is either complete or not there at all. Writing to the same id again stores a new version and reads return the newest; add "content": true to leave an id that already holds a value alone.
Read it back
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op": "object_get", "ids": [900000000501]}'Expected: the value comes back present, exactly as stored
The value comes back base64 encoded, as it went in: pipe it through jq -r '.objects[0].value_b64' | base64 -d to see the sentence.
Every id in a read answers with a state, in the order you asked: present with its value, absent, deleted, too_large, or a named state when a value could not be read, never a silent gap. object_delete removes ids; a later read answers deleted, with the version it was deleted at.
Within a bound you choose
A read can name how many bytes it will accept, up to a bounded size per call. A value larger than that is not cut short: it is named, with its length, so you know to ask again with more room.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op": "object_get", "ids": [900000000501], "max_bytes": 64}'Expected: the value is named as too large for a sixty-four byte read, with its length
Where it stands
We have taken our published benchmark rows down while every domain is re-measured on new hardware, with a dataset in RAM and one bigger than RAM running at the same time, as a stress test of Semurg itself. The re-measured object results will be on the benchmark page, wins and losses alike, with the release and the machine they ran on. Until then this post states no comparison.
What it does not do yet
- List keys, list by prefix, or read a byte range of a value.
- Speak the S3 API, or import from S3 or MinIO.
- Use string keys: ids are whole numbers, so hash or map your names to them.
- Accept a value larger than one request over the network: very large values are stored from inside your application.
- Carry metadata beyond one number per value.
Next
- A document store that reads by id: the same door for small JSON documents.
- Four kinds of data, one route: objects, streams, search and places, one call each.
How these commands were checked
Our test farm ran each of the 3 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.