Security model
Two independent layers, both worth understanding before writing a client:
Socket file permissions
reamer-stream sets the socket file to 0600 (owner read/write only) immediately after bind() — not left to whatever the process's umask happens to produce. This restricts access between users: only the account the server runs as (and root) can even open the socket.
Optional shared-secret token
File permissions alone don't restrict access between processes running as the same user. Setting the REAMER_STREAM_AUTH_TOKEN environment variable before starting reamer-stream closes that gap: every new connection must then send an Auth message with a matching token as its first frame — anything else sent first gets the connection closed immediately, no response. Unset (the default): no auth required. There is no --auth-token=<...> CLI flag deliberately — a secret value passed via argv is visible to any other process on the machine (/proc/<pid>/cmdline, ps), which would defeat the token's own purpose.
0600 permissions stop a different OS user from ever opening the socket. The optional token additionally stops a different process running as the same user. Neither stops a full compromise of the account the server itself runs as. There's no encryption of data in flight, because there's no network hop for it to cross.
At rest. .bin files, .reamer result files, and audit.log are all stored unencrypted. Encrypting them — e.g. filesystem-level encryption at the deploying firm's own OS layer — is explicitly the deploying firm's responsibility, not something Reamer Stream does itself.
Transport & configuration
A Unix domain socket, same machine only — no TCP/remote option exists in v1. Default path: /tmp/reamer-stream.sock, overridable via reamer-stream --socket-path=<path>.
| Flag | Default | Notes |
|---|---|---|
--data-dir=<path> | REAMER_STREAM_DATA_DIR env var, else ./data | Where .bin files are read/written. If you also run reamer_py against the same data, point it at this same directory (reamer_py.set_data_dir()). |
--socket-path=<path> | /tmp/reamer-stream.sock | The socket external tools connect to. |
Precedence for the data directory: --data-dir flag, then REAMER_STREAM_DATA_DIR, then ./data.
The server also caps concurrent connections at 256 (not configurable) — a same-machine ceiling, not sized for internet-facing exposure. A connection opened past the cap is accepted then immediately closed with no data sent.
Framing
| Field | Size | Notes |
|---|---|---|
| version | 1 byte | Must be 0x01. Server closes the connection immediately on any other value. |
| length | 4 bytes, unsigned, big-endian | Byte length of payload that follows. |
| payload | length bytes | Message-type-specific. |
There is no pagination in either direction — a HistoricalResponse can be arbitrarily large, and an IngestCsv/IngestBars request is capped only at a generous 128 MB. Read exactly length bytes before attempting to parse the payload; a large message may arrive across multiple reads.
All multi-byte integers are big-endian. All floating-point fields are IEEE 754 binary64, also big-endian — exactly what struct.pack('>d', x) does in Python. Never treat the wire bytes as directly castable to a C struct, even on a little-endian machine. A string field is length-prefixed: 2 bytes unsigned big-endian length, then that many raw UTF-8 bytes, no null terminator.
QueryHistorical / HistoricalResponse
Client → server: QueryHistorical
| Field | Size | Notes |
|---|---|---|
| message_type | 1 byte | 0x00 |
| ticker | string | Exact ticker name, case-sensitive. |
| has_start | 1 byte | 0x00 or 0x01. |
| start_ts | 8 bytes, signed | Epoch seconds (UTC), inclusive. Meaningful only when has_start=1. |
| has_end | 1 byte | 0x00 or 0x01. |
| end_ts | 8 bytes, signed | Epoch seconds (UTC), inclusive. Meaningful only when has_end=1. |
has_start=0 means "from the first bar available"; has_end=0 means "through the last bar available." Both 0 returns the ticker's entire series.
Server → client: HistoricalResponse
| Field | Size | Notes |
|---|---|---|
| message_type | 1 byte | 0x00 |
| found | 1 byte | 0x01 if the ticker is known, 0x00 if not. |
| bar_count | 4 bytes, unsigned | Number of bar records that follow. 0 when found=0, and legitimately also 0 when the ticker is known but nothing falls in range — check found first. |
| bars | bar_count × 64 bytes | See below. |
Each bar record, in order (64 bytes total — an explicit field-by-field big-endian encoding, not a raw copy of the in-memory struct):
| Field | Size |
|---|---|
| ts | 8 bytes, signed (epoch seconds, UTC) |
| open, high, low, close, volume, notional | 8 bytes each, double |
| tick_count | 8 bytes, signed (-1 = not supplied) |
Bars are returned in ascending ts order.
IngestBars & IngestCsv
IngestBars is for a connector that already has structured bars — a Databento or Alpaca connector pulling from a vendor API that returns typed records directly. IngestCsv is for anything with a CSV file and no structured-bar step of its own — the server parses it using the same auto-detecting parser every other Reamer Stream ingestion path uses.
| Field | Size | Notes |
|---|---|---|
| message_type | 1 byte | 0x01 (IngestBars) or 0x02 (IngestCsv) |
| ticker | string | Overwrites any existing data under the same ticker. |
| bar_count / csv_length | 4 bytes, unsigned | Number of bar records, or byte length of csv_bytes. |
| bars / csv_bytes | variable | Same 64-byte bar layout as HistoricalResponse, or the raw CSV file content. |
Server → client: IngestAck
| Field | Size | Notes |
|---|---|---|
| message_type | 1 byte | 0x01 |
| success | 1 byte | 0x01 if written and now queryable, 0x00 otherwise. |
| reason | string | Empty on success; a human-readable failure description otherwise. |
Ingested data is immediately queryable via QueryHistorical — including on the same connection, right after the IngestAck.
IngestBars/IngestCsv also share a process-wide 512 MB ceiling on combined ingestion memory in flight at once. A request that would push the total over that ceiling gets IngestAck{success=false} with a reason describing the budget exceeded, rather than proceeding and risking an OOM. A single request never trips this on its own.
Subscribe / Unsubscribe / Publish
A stream name is a plain string with no relationship to a ticker unless a caller chooses to reuse the same string for both.
| Message | Fields |
|---|---|
| Subscribe / Unsubscribe | message_type (0x03/0x04), stream (string) |
| Publish | message_type (0x05), stream (string), bar (64 bytes) |
| SubscriptionAck / PublishAck | message_type (0x02/0x03), success (1 byte), reason (string) |
Subscribing twice to the same stream on the same connection is harmless (idempotent). Publishing to a stream with zero current subscribers is not a failure — PublishAck{success=true} either way; the record is simply dropped. No persistence: a subscriber not listening at publish time never sees that record.
Server → client: PushedRecord (unsolicited)
| Field | Size | Notes |
|---|---|---|
| message_type | 1 byte | 0x04 |
| stream | string | The stream name this record was published to. |
| bar | 64 bytes | Same 8-field layout as HistoricalResponse's bars. |
message_type byte of every incoming frame and route PushedRecords separately from whatever response is currently pending.
Auth
| Message | Fields |
|---|---|
| Client → server: Auth | message_type (0x06), token (string — must exactly match REAMER_STREAM_AUTH_TOKEN) |
| Server → client: AuthAck (success only) | message_type (0x05), success (always 0x01), reason (always empty) |
A wrong token gets no AuthAck at all — the connection is simply closed, so a peer can't distinguish "wrong token" from "malformed frame" from the outside.
Concurrency semantics
reamer-stream is single-threaded: StreamServer::poll() is one epoll_wait loop, and every accepted event — including the blocking StreamEngine call inside it — runs to full completion before the loop returns to epoll_wait for the next one. There is no thread pool, no per-connection thread.
IngestBars/IngestCsv replace the engine's entire bar index in one step, and because the server never runs two requests at once, a QueryHistorical on connection B while an IngestBars is in flight on connection A always sees either the complete pre-ingest data or the complete post-ingest data — never a mix.
What this does not claim: no ordering guarantee between two connections' requests that both arrive before the server gets to either one — which one epoll_wait happens to report first is unspecified. The guarantee is about atomicity of each individual request, not about which one wins a race to be first.
This is a property of the single-threaded design, not a lock: StreamEngine itself has no internal synchronization and is not safe to call concurrently with no external locking — reamer-stream's own single-threaded dispatch is what makes the guarantee above hold.
Failure modes
A client can tell which of two categories a failed request falls into from the response shape alone: silence (connection closes, nothing sent back) always means the request itself couldn't be trusted enough to interpret; an explicit success=false/found=false field always means the request was understood and something at the data layer said no.
Connection closed, no response sent
| Trigger | Notes |
|---|---|
Wrong protocol version byte | Any value other than 0x01. |
| Oversized payload | Declared length exceeds the 128 MB request cap. |
| Truncated/malformed payload field | Checked identically for every request type. |
Unrecognized message_type byte | Not one of the eight message types this protocol defines. |
| Non-Auth message before authenticating | Only when REAMER_STREAM_AUTH_TOKEN is configured. |
| Wrong Auth token | No AuthAck sent either way. |
| Connection count at the 256-connection cap | Accepted then immediately closed, zero bytes sent. |
Normal ack/response with a failure field, connection stays open
| Trigger | Client-observable result |
|---|---|
| Unknown ticker on QueryHistorical | HistoricalResponse{found=false, bar_count=0} — not an error. |
| Known ticker, empty result range | HistoricalResponse{found=true, bar_count=0} — check found first. |
| Ingest budget ceiling exceeded | IngestAck{success=false, reason=<budget message>} |
| IngestCsv parse failure | IngestAck{success=false, reason=<parser message>} |
| Ingest write failure | IngestAck{success=false, reason=<error message>} |
| Publish to a stream with no subscribers | PublishAck{success=true} — not a failure. |
| Redundant Subscribe/Unsubscribe | SubscriptionAck{success=true} — idempotent. |
Worked example
Request: QueryHistorical for ticker "AAPL", no bounds:
Response: ticker found, one bar (ts=1700000000, all price/volume fields 0.0, tick_count=1):