Streams
A stream is an append-only byte sequence addressed by a URL: /{bucket}/{stream}. Once data is written it cannot be modified or reordered. Only new data can be appended.
Naming
Stream IDs may contain any byte that is not /, \0, or ... Maximum stream ID length is 122 bytes, and the combined bucket-plus-stream path is also capped at 122 bytes. The literal name streams is reserved and cannot be used as a stream ID. A stream ID cannot start with $ either; that prefix is reserved for bucket-level subresources (400).
A workspace app might use /workspace-a/doc-123. An agent system might use /agents/run-2026-05-13-abc. Anything within the rules above is fair.
Content type
A stream's content type is set when it is created with PUT and defaults to application/octet-stream if no Content-Type header is supplied. POST never creates a stream: an append to a stream that does not exist answers 404. Subsequent appends must declare the same content type, or the server rejects them with 409. The content type rides along on reads so clients can dispatch on it without inspecting payloads.
Lifecycle
Every stream is in one of these states:
- Open. Accepts appends. The default on creation.
- Closed. Sealed by
Stream-Closed: trueon a POST. Readers receive an EOF signal. Further appends return409. Close is irreversible. - Expired. Past its
Stream-TTLorStream-Expires-Atdeadline. Reads and writes return404. The group's leader removes an expired stream within about a minute, and cold GC then deletes its cold objects. - Deleted. Removed by
DELETE. Reads return404.
Stream-TTL (seconds, relative) and Stream-Expires-At (RFC 3339 absolute) are mutually exclusive. Sending both yields 400.
Stream-TTL is a sliding window, not a deadline fixed at creation:
- Every accepted append or close renews it; a deduplicated retry or a repeat close does not.
- Reads renew it at most once per quarter of the TTL, so a stream that is only read can expire after about 0.75×TTL of idleness following the last read.
HEADdoes not renew it. - An SSE connection renews it when it opens, not while it waits for new data. Ursula does not close idle SSE connections (the base protocol expects servers to end them about every 60 seconds so readers reconnect), so an open subscription alone does not keep a stream with no writes alive. Each long-poll request renews like any other read.
- Renewal never moves expiry earlier, even when the node that handled the request has a slower clock than the one that last renewed it.