Write it once
Every mutation answers with its result
Dot keys address inside one JSON document per base key. add returns the new number, delete whether the key existed, push the array. Your bot never needs a second round trip to know where it stands.
import { HorizonDB } from "horizondb"; const db = await HorizonDB.connect({ url: "ws://127.0.0.1:8080", username: "admin", password: "secret", }); await db.set("guild.42.settings.prefix", "!"); await db.add("guild.42.stats.messages"); // → 1 await db.push("guild.42.mods", "1234", { returnValue: true }); // → ["1234"] await db.cache("cooldown.42.daily", true, 86_400_000); await db.get("guild.42"); // { settings: {…}, stats: { messages: 1 }, mods: ["1234"] }
→ { "id": "1", "operation": "login", "login": "admin", "password": "secret" } ← { "id": "1", "type": "response", "data": { "success": true, "sessionId": "9f1c…" } } → { "id": "2", "operation": "add", "sessionId": "9f1c…", "table": "json", "key": "guild.42.stats.messages", "amount": 1, "timeoutMs": 10000 } ← { "id": "2", "type": "response", "data": { "success": true, "value": 2 } } // any language with a WebSocket can be a client
Cross-section
Three layers, one line between them
A bot hammers a few large documents from many shards at once. A SQL round trip per add is too slow, and memory alone forgets on restart. HorizonDB keeps both and draws the line where it costs least.
Memory
above the horizon
Every get, set, add and push runs here. Each document sits behind its own lock: readers copy only the sub-tree they asked for, writers edit in place, so a push into a multi-megabyte guild costs microseconds. Keys not loaded yet are read once from PostgreSQL, single-flighted, then stay hot.
The flush
the horizon
Every flush_interval_secs and on SIGINT/SIGTERM, dirty keys go down per table: UNNEST upserts and ANY deletes. Dirty marks carry a generation, so a write that lands mid-flush stays dirty for the next cycle.
PostgreSQL
below the horizon
Plain tables, created on demand: an id and a JSON value. Query them, back them up, restore them with the tools you already run. Table names are validated before they ever reach SQL.
Built for load
Fast where it counts, strict everywhere else
No lost updates
Read-modify-write is serialized per key. Sixteen workers hitting one counter end on the exact sum, checked on every benchmark run.
Pipelined by design
Requests are correlated by id, so one socket carries up to 1 024 in flight. Past that the server stops reading and TCP pushes back.
Deadlines respected
A request carrying timeoutMs that already waited too long gets deadline exceeded instead of work nobody is waiting for.
Auth done properly
argon2id password_hash or constant-time comparison, UUID sessions that expire on their own.
A client that heals
The SDK re-logs once on an expired session, reconnects with backoff when a socket goes silent, and throws typed errors. Zero runtime dependencies.
Safe Rust, pedantic clean
unsafe is forbidden, clippy pedantic passes, internal errors stay in the logs and clients get a generic message.
Durability is a trade you choose
Until the next flush, writes live only in memory. A crash or kill -9 loses up to flush_interval_secs of them; a graceful stop flushes everything first, and the systemd unit gives it five minutes to do so. Lower the interval if that window is too wide for your data.
Measured, not promised
Tens of microseconds on the hot path
End to end through the SDK, against a real server and PostgreSQL, network included.
| Path | Throughput | p50 | p95 |
|---|---|---|---|
hot get, one connection | 18.1 k ops/s | 42 µs | 67 µs |
has, one connection | 20.1 k ops/s | 39 µs | 53 µs |
| login + logout | 17.2 k ops/s | 43 µs | 66 µs |
| raw WebSocket client, same server | 48 k ops/s | — | — |
Linux laptop, consumer NVMe, PostgreSQL 16 in a container, Bun, Rust debug build, --quick profile. Full table, method and known issues in BENCHMARKS.md. Reproduce with ./scripts/bench.sh --quick. Release-build latency, throughput and memory charts: benchmarks.
Quick start
From clone to first key
Start PostgreSQL and the server
The helper script runs a PostgreSQL container, writes config.toml, builds and starts the server.
git clone https://gitlab.com/ihrz/db.git horizondb && cd horizondb
./scripts/dev-up.sh # ws://127.0.0.1:8080, logs in .server.log
Add the client to your bot
Bun runs the TypeScript straight from the repository. Pin a tag for reproducible builds.
"dependencies": { "horizondb": "git+https://gitlab.com/ihrz/db.git#main" }
Go to production
Copy example.config.toml, set an argon2id password_hash, size max_message_size above your largest document, and install horizondb.service.
cargo build --release sudo cp horizondb.service /etc/systemd/system/ && sudo systemctl enable --now horizondb