---
title: "An object store on your own disks: large values, striped reads, and where it stands"
slug: semurg-as-an-object-store
date: 2026-10-12
author: Semurg
section: how-to
tags: [object, storage, database, how-to]
summary: Store values of any size under your own ids, read them back in one sweep across every disk, replace or delete them, and see what the object door does not do yet.
status: published
covers: [route:POST /v1/query, op:object_put, op:object_get, op:object_delete]
---
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.

> Diagram object-layout: A short value lives inside its record; a longer one is cut into fixed-size pieces spread evenly across every disk, then a short manifest, then the record that names it, written last, so a value is complete or absent and reading it is one sweep with every disk 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:

```bash runnable kind=shell-node needs=token
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="}]}'
```

```expect
expect: the value is written
assert: json:.ok==true and .written==1
```

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

```bash runnable kind=shell-node needs=token
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]}'
```

```expect
expect: the value comes back present, exactly as stored
assert: json:.objects[0].state=="present" and .objects[0].value_b64=="U2VtdXJnIG9iamVjdCBleGFtcGxlOiBhIHZhbHVlIGxvbmdlciB0aGFuIHR3ZW50eSBieXRlcyBpcyBhbiBhbmNob3IgcGx1cyBhIHJ1biBvZiBwaWVjZXMgc3RyaXBlZCBhY3Jvc3MgZXZlcnkgZGlzay4="
```

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.

```bash runnable kind=shell-node needs=token
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}'
```

```expect
expect: the value is named as too large for a sixty-four byte read, with its length
assert: json:.objects[0].state=="too_large"
```

## 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](/benchmark), 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](/blog/semurg-as-a-document-store): the same door for small JSON documents.
- [Four kinds of data, one route](/blog/objects-streams-search-and-places): objects, streams, search and places, one call each.
