VeltrixDB Docs
v1.16 GitHub FAQ Quickstart

Wire Protocol

VeltrixDB speaks two protocols over the same TCP port: a human-readable text protocol for ad-hoc use with nc or telnet, and a compact binary protocol that every official SDK uses by default. The server auto-detects which one a connection is speaking from the first byte.

Text protocol

The text protocol is line-oriented — one command per line, a plain-text response per line — which makes it usable directly from nc, telnet, or any tool that can write to a TCP socket. It's the fastest way to sanity-check a running server.

CommandResponse
PUT key valueOK
GET keyvalue (or an ERR ... line if not found)
DEL keyOK
PINGPONG
INFOkeys=N writes=N reads=N ...
AUTH user passOK
QUITBYE

SET is an alias of PUT, DELETE of DEL, EXIT of QUIT, and CLUSTER of TOPOLOGY. Commands are case-insensitive. A malformed command answers ERR usage: ...; an unknown one answers ERR unknown command: ... with the supported list.

Text commands added in v1.1.0

v1.1.0 brings the text protocol to parity with operations that were previously binary-only (TTL writes, atomics, batch get) and adds the new list and set data types. The usage lines below are exactly what the server prints on a malformed invocation (ERR usage: ...):

CommandResponse
PUTEX <key> <ttl-seconds> <value>OK — a PUT with expiration. The binary PUT frame (0x01) has no TTL field; binary clients send a TTL in MPUT’s per-entry header ([2B keyLen][4B valLen][4B ttl]). TTL must be ≥ 1; ERR bad ttl otherwise.
INCR <key> [delta] / DECR <key> [delta]The new integer value, as a decimal line. delta defaults to 1; a missing key counts from zero.
SETNX <key> <value>1 (created) or 0 (key already exists).
CAS <key> <expected> <new>OK (swapped), MISMATCH (current value differed), or NOTFOUND (key absent).
MGET <key> [key ...]One <key> <value> line per found key (missing keys are silently omitted), terminated by END.
LPUSH <key> <value> / RPUSH <key> <value>The new list length.
LPOP <key> / RPOP <key>The popped value, or NIL when the list is empty.
LLEN <key>The element count.
LRANGE <key> <start> <stop>One element per line, then END. Redis semantics: stop inclusive, negative indices count from the end.
SADD <key> <member> / SREM <key> <member>1 when added/removed, 0 otherwise.
SISMEMBER <key> <member>1 or 0.
SMEMBERS <key>One member per line (unordered), then END.
SCARD <key>The member count.
Token-delimited arguments CAS parses <expected> as a single space-delimited token — use the binary protocol for values containing spaces. Semantics of every command (per-key TTL, atomic RMW guarantees, list/set encoding) are covered in the Data Model, and search semantics are on Vector & Hybrid Search.

Search, index and query commands

Search commands answer id score lines then END, or one ERR ... line. What they do — ranking, filters, memory layouts, cluster fan-out — is on Vector & Hybrid Search.

CommandResponse
VCREATE <ns> <dim> [QUANT none|int8|pq] [PQM <m>] [PQTRAIN <n>] [GRAPH memory|disk] New in v1.11OK. PQM/PQTRAIN/GRAPH and QUANT pq are new in v1.12.
VSET <key> [NS <ns>] <f1> <f2> ...OK. NS is new in v1.11 (default namespace default).
VSEARCH <k> [NS <ns>] [EF <n>] [FILTER <field> <op> <value>] <f1> <f2> ...id score lines (cosine), END. k = 0 returns every match. NS/EF/FILTER are new in v1.11.
VDEL <key> [NS <ns>] New in v1.11OK
TSET <id> [NS <ns>] TEXT <text> New in v1.11OK. The text is the rest of the line (at most 1 MiB).
TDEL <id> [NS <ns>] New in v1.11OK
TSEARCH <k> [NS <ns>] [FILTER <field> <op> <value>] QUERY <text> New in v1.11id score lines (BM25), END. k = 0 returns every match.
HSEARCH <k> [NS <ns>] [EF <n>] [ALPHA <a>] [CAND <n>] [FILTER <field> <op> <value>] [VEC <f1> ... <fn>] QUERY <text> New in v1.11id score lines (RRF score), END. k > 0; needs VEC, QUERY or both.
IDXCREATE <name> <field> / IDXDROP <name>OK. Indexes <field> of every value (JSON object or k=v pairs); backfills existing keys.
IDXQUERY <name> <value> [LIMIT n]One key per line, END.
QUERY <namespace> WHERE <field> <op> <value> [LIMIT n]key value lines, END. Ops: = != > < >= <= contains; uses a secondary index for = on an indexed field, else scans the namespace.

Option clauses may come in any order, each at most once; the floats or the QUERY text come last. Because the text protocol is whitespace-split, filter values cannot contain spaces and TSET text cannot contain newlines — use the binary search opcodes for those.

Other text commands

CommandResponse
NSPUT <ns> <key> <value> / NSGET <ns> <key> / NSDEL <ns> <key> / NSDROP <ns>Namespaced KV (see Data Model).
NSSCAN <ns> [prefix] [limit] / NSLISTkey value lines / namespace count lines, then END.
HSET <key> <field> <value> [ttl], HGET, HDEL, HGETALL, HKEYS, HLEN, HEXPIRE <key> <field> <ttl>, HTTLHash fields with per-field TTL. HGET of a missing field answers NOT_FOUND.
RANGE <start> <end> [LIMIT n] [REV]Ordered key value lines, END (local node only in cluster modes).
SCANCUR <cursor> <limit>key value lines, then CURSOR <next>; cursor - starts from the beginning.
TXN <numOps> then that many lines of SET <key> <value> / SETIF <key> <expectedVersion> <value> / DEL <key>OK, or CONFLICT when a SETIF version no longer matches.
VER <key>The key's version (the token for SETIF); 0 when absent.
TOPOLOGY / CLUSTEROne JSON line with the cluster topology, for client-side routing.

Worked example

Start a server and drive it with nc:

$ docker run -p 9000:9000 ghcr.io/veltrixdb/veltrixdb:latest

$ nc localhost 9000
PUT hello world
OK
GET hello
world
PUT counter 41
OK
GET counter
41
DEL hello
OK
GET hello
ERR
PING
PONG
INFO
keys=1 writes=3 reads=3 ...
QUIT
BYE

Or pipe a whole session in one shot, as shown in the project's own quick-start:

echo -e "PUT hello world\nGET hello\nPING" | nc localhost 9000

Binary protocol

The binary protocol is what every official SDK (Go, Python, Node.js, Java, Rust, C++) speaks by default (only the Go client implements the search opcodes), and it's what the server assumes once it sees a leading byte that matches a known opcode rather than ASCII command text — no handshake or negotiation is required, the server simply auto-detects the framing per connection.

Frame layout

Request:  [1B cmd][2B keyLen][4B valLen][key][value]
Response: [1B status][4B payloadLen][payload]

Opcodes

Every opcode below is defined in cmd/server/main.go (handlers for 0x20–0x29, 0x08, 0x1C, 0x1E and 0x1F in cmd/server/ext_ops.go). 0x1D is unused. New opcodes take free slots at or below 0x29, because a first byte above it is read as text.

CategoryOpcodeCommand
Single-key0x01PUT
0x02GET
0x03DEL
0x04PING
0x05INFO
Batch0x06MPUT (vectorized multi-put)
0x07MGET (vectorized multi-get)
Auth0x09AUTH — [1B 0x09][2B userLen][4B passLen][user][pass]
Namespace0x0ANSPUT
0x0BNSGET
0x0CNSDEL
0x0DNSDROP
0x0ENSSCAN
0x0FNSLIST
Hash0x10HSET
0x11HGET
0x12HDEL
0x13HGETALL
0x14HKEYS
0x15HLEN
0x16HEXPIRE
0x17HTTL
Atomic0x18CAS (compare-and-swap)
0x19INCR
0x1ADECR
0x1BSETNX
Extended0x20RANGE
0x21SCANCUR
0x22TXN (one-shot optimistic transaction)
0x23IDXCREATE
0x24IDXDROP
0x25IDXQUERY
0x28QUERY
0x29GETVER (key version for TXN SETIF)
Vector0x26VSET (namespace default)
0x27VSEARCH (namespace default)
0x08VSETNS New in v1.11
0x1EVSEARCHX — namespace, ef, filter New in v1.11
0x1FVDEL New in v1.11
Search0x1CSEARCH, sub-op in the 2-byte field: 1 VCREATE, 2 TSET, 3 TDEL, 4 TSEARCH, 5 HSEARCH New in v1.11

The atomic opcodes 0x18–0x1B use extended frames that carry the extra operands (expected value, delta, TTL) — as of v1.1.0 the same four operations are also available as text commands (CAS/INCR/DECR/SETNX, above) and in every official SDK:

CAS:   [1B 0x18][2B keyLen][4B expectedLen] + [4B newLen][4B ttl] + key + expected + new
INCR:  [1B 0x19][2B keyLen][4B 0]           + [8B delta int64] + [4B ttl] + key
DECR:  [1B 0x1A][2B keyLen][4B 0]           + [8B delta int64] + [4B ttl] + key
SETNX: [1B 0x1B][2B keyLen][4B valLen]      + [4B ttl] + key + value

Vector and search frames

Each frame starts with the standard 7-byte header [1B cmd][2B][4B], whose two fields are reinterpreted per opcode; all integers are little-endian, floats are IEEE-754 float32 LE. Writes answer the standard OK/ERR frame.

VSET     0x26: [1B][2B keyLen][4B dim] + key + dim×4B
VSEARCH  0x27: [1B][2B k][4B dim]      + dim×4B
VSETNS   0x08: [1B][2B idLen][4B dim]  + [2B nsLen] + ns + id + dim×4B
VSEARCHX 0x1E: [1B][2B k][4B dim]      + [2B nsLen][2B ef][2B fieldLen][2B opLen][2B valLen]
               + ns + field + op + value + dim×4B      (fieldLen 0 = no filter, ef 0 = default)
VDEL     0x1F: [1B][2B idLen][4B nsLen] + ns + id

SEARCH   0x1C: [1B][2B subop][4B payloadLen] + payload of [4B len][bytes] fields
  1 VCREATE: ns, dim u32, quant ("" | none | int8 | pq)
             [, pqM u32, pqTrainAt u32, graph ("" | memory | disk)]          → OK
  2 TSET:    ns, id, text                                                     → OK
  3 TDEL:    ns, id                                                           → OK
  4 TSEARCH: ns, k u32, query, filterField, filterOp, filterValue             → matches
  5 HSEARCH: ns, k u32, ef u32, alpha f32 (NaN = default 0.5), candidates u32,
             vec (dim×4B, may be empty), query, filterField, filterOp, filterValue → matches
  (filterField "" = no filter)

Matches (VSEARCH, VSEARCHX, TSEARCH, HSEARCH):
  [1B OK][4B count] + count × [2B idLen][4B score float32][id]

The trailing VCREATE fields (pqM, pqTrainAt, graph) are new in v1.12 and optional, so older clients that send only the first three fields keep working. The Go client is the only official SDK that implements these frames (VSetNS, VSearchWithOptions, VDel, VCreateWithOptions, TSet, TDel, TSearch, HSearch).

Status codes

CodeStatusMeaning
0x00OKRequest succeeded; payload holds the result, if any.
0x01ERRRequest failed; payload holds an error message.
0x02NOT_FOUNDKey does not exist or is expired/tombstoned.
0x03EXISTSSETNX: the key already existed; nothing was written.
0x04MISMATCHCAS: the expected value did not match the current value; nothing was written.
0x05CONFLICTTXN: an optimistic version check failed — re-read and retry.
Prefer batches An MPUT/MGET batch pays one round trip (and, for writes, one group commit) for many keys instead of one per key, so it is much faster than the same number of individual PUT/GET calls; the gain depends on batch size and network latency. All official SDKs expose batch methods (for example, db.mput(...) / db.mget(...)) — reach for them whenever you're writing or reading more than a handful of keys at once.

Protocol auto-detection

Both protocols share the same TCP listener (-addr, default :9000). A single connection is either all-text or all-binary — the server determines this from the leading byte of the first request — 0x01–0x29 selects binary, anything else is read as a text line — and does not switch mid-connection. You never need to configure which protocol to use; connecting with nc/telnet naturally speaks text, and every official SDK's connection pool speaks binary. This applies to the default --net=go front-end; see below for the opt-in C++ front-end.

Which front-end serves which commands New in v1.7

The --net server flag selects the network front-end that listens on -addr:

--netImplementationServes
go defaultGo server, one goroutine per connectionEverything on this page: the text protocol, every binary opcode (including the extended and atomic frames), AUTH, TLS on --tls-addr, and all --mode values.
cpp / uring / pollC++ event loops in netfront/, one per core (--net-threads); cpp uses io_uring when available and poll() otherwise, uring requires io_uring, poll forces poll()Only the binary protocol's PUT, GET, DEL, PING, MPUT and MGET. No text protocol, no AUTH, no extended binary commands.
The C++ front-end is experimental It runs only in --mode=standalone and the server refuses it with --auth-config; TLS stays on the Go listener. Each connection stays strictly ordered, and reads that need a disk read are finished on a goroutine rather than the event loop (VELTRIXDB_NET_DEFER_READS=0 disables this for A/B runs). It is not faster overall: on a 4-CPU CI runner it lowered read P50 against --net=go but had a worse read P99 and ~12% lower batch-write throughput. To load-test it, use loadtest --proto=binary.