Durable Streams Extensions
Extensions to the Durable Streams Protocol: bucket namespacing, snapshot semantics, and bootstrap delivery.
Document: Durable Streams Protocol Extensions Status: Draft Date: 2025-03-14
Abstract
This document defines extensions to the Durable Streams Protocol. It adds bucket namespacing, snapshot and bootstrap semantics, and multipart bootstrap delivery. These extensions are general-purpose and applicable to any append-only stream workload, including CRDT synchronization, event sourcing, and agent session replay.
All base protocol semantics remain in effect. Where this document is silent, the base protocol governs.
Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174].
Table of Contents
1. Buckets
Every stream belongs to a bucket. The stream URL takes the form {base_url}/{bucket_id}/{stream_id}.
1.1. Identifier Constraints
bucket_id: MUST match^[a-z0-9_-]{4,64}$. Globally unique within the service.stream_id: Any UTF-8 string. MUST NOT exceed 122 bytes. MUST NOT contain/,\0, or... On the bucketed/surface, the combined{bucket_id}/{stream_id}key MUST also not exceed 122 bytes.- The literal local stream ID
streamsis reserved for the bucket listing endpoint/{bucket_id}/streams.
1.2. Create Bucket
PUT /{bucket_id}
Buckets MUST be created explicitly. PUT /{bucket_id}/{stream_id} MUST NOT implicitly create a bucket.
Response Codes:
201 Created: Bucket created successfully.400 Bad Request:bucket_idis invalid.409 Conflict: Bucket already exists.
1.3. Get Bucket Metadata
GET /{bucket_id}
Response Codes:
200 OK: Bucket exists. Returns a JSON object with at leastbucket_id(string) andstreams(integer, count of streams in the bucket).400 Bad Request:bucket_idis invalid.404 Not Found: Bucket does not exist.
1.4. List Bucket Streams
GET /{bucket_id}/streams?prefix={prefix}&after={cursor}&limit={n}
prefixfilters bucket-local stream IDs by prefix. When omitted, all bucket-local stream IDs are eligible.afteris an exclusive cursor over bucket-local stream IDs.limitdefaults to1000and must be in1..=1000.
The response body is:
{
"bucket_id": "agents",
"prefix": "user-",
"stream_count": 2,
"streams": [
{
"stream_id": "user-1",
"status": "Open",
"content_type": "text/plain",
"tail_offset": 42,
"created_at_ms": 1735689600000,
"last_write_at_ms": 1735689601000
}
],
"next_cursor": "user-2",
"has_more": true
}
streams is sorted lexicographically by stream_id.
Response Codes:
200 OK: Matching streams returned.400 Bad Request:bucket_idor query parameters are invalid.404 Not Found: Bucket does not exist.
1.5. Delete Bucket
DELETE /{bucket_id}
A bucket MUST be empty (zero streams) before deletion.
Response Codes:
204 No Content: Bucket deleted.400 Bad Request:bucket_idis invalid.404 Not Found: Bucket does not exist.409 Conflict: Bucket is not empty.
1.6. Stream Operations
All stream operations defined in the base protocol apply under {base_url}/{bucket_id}/{stream_id}. When the bucket does not exist, the server MUST return 404 Not Found.
2. Snapshots
A snapshot is a materialized representation of a stream's content from offset -1 (inclusive) to snapshot_offset (exclusive). Snapshots enable clients to skip full replay and resume from a compacted state.
2.1. Offset Conventions
- Offsets are opaque tokens as defined in the base protocol. Clients MUST use server-returned
Stream-Next-Offsetvalues.
Servers that expose snapshots and /bootstrap as message sequences MUST use one consistent retained-message boundary model across ordinary reads, snapshot-offset validation, and bootstrap responses. This extension does not require any particular binary framing format.
2.2. Publish Snapshot
PUT {stream_url}/snapshot/{snapshot_offset}
Creates a snapshot at the specified offset. The snapshot represents the materialized result of folding all updates in the range [-1, snapshot_offset).
Request Headers:
Content-Type: The content type of the snapshot blob. Servers MUST store this value and return it on subsequent reads. Defaults toapplication/octet-stream.
Response Codes:
204 No Content: Snapshot published successfully.400 Bad Request:snapshot_offsetis invalid or not aligned to a committed message boundary.404 Not Found: Stream does not exist.409 Conflict:snapshot_offsetexceeds the current tail, or the server cannot produce a consistent view at that offset.410 Gone:snapshot_offsetis older than the current earliest retained offset.413 Payload Too Large: Snapshot exceeds the server's size limit.
Retention Effect:
When a new snapshot is published, the server MUST treat snapshot_offset as the new earliest retained offset. For any offset less than snapshot_offset (including -1), the server MUST return 410 Gone, forcing clients to re-initialize via /bootstrap.
Publishing a new snapshot replaces the previously visible snapshot immediately. The superseded snapshot MAY be garbage-collected asynchronously after the new snapshot becomes visible. Clients MUST NOT depend on older snapshots remaining readable after overwrite.
Concurrency:
Snapshot creation MUST NOT block concurrent appends. The snapshot's consistency boundary is snapshot_offset; updates at or beyond that offset MUST NOT be folded into the snapshot.
2.3. Read Latest Snapshot
GET {stream_url}/snapshot
Returns a redirect to the latest visible snapshot resource, if one exists.
Response:
307 Temporary Redirectwhen a latest snapshot exists. TheLocationheader points to{stream_url}/snapshot/{snapshot_offset}.404 Not Foundwhen no snapshot exists.
2.4. Snapshot Metadata on HEAD
HEAD {stream_url}
Servers SHOULD include Stream-Snapshot-Offset when a latest visible snapshot exists for the stream.
Response Headers:
Stream-Snapshot-Offset: The latest visible snapshot offset.
Semantics:
- Absence of
Stream-Snapshot-Offsetmeans the stream has no visible snapshot. - When present, the value MUST refer to the same latest visible snapshot that
GET {stream_url}/snapshotwould resolve to.
2.5. Read Snapshot
GET {stream_url}/snapshot/{snapshot_offset}
Returns the snapshot blob at the specified offset.
Response Headers:
Content-Type: The content type stored at publish time.Stream-Snapshot-Offset: The snapshot offset.Stream-Next-Offset: The next offset after the snapshot.Stream-Up-To-Date: Boolean indicating whether the stream has updates beyond the snapshot.
Response Codes:
200 OK: Snapshot exists.404 Not Found: Snapshot does not exist or has been garbage-collected.
2.6. Delete Snapshot
DELETE {stream_url}/snapshot/{snapshot_offset}
Deletes the snapshot identified by snapshot_offset, subject to bootstrap safety rules.
Response Codes:
404 Not Found: Snapshot does not exist or has been superseded.409 Conflict:snapshot_offsetrefers to the latest visible snapshot and cannot be deleted because/bootstrapwould become incomplete.
Implementations that expose only one visible snapshot MAY make superseded snapshots unreachable immediately after overwrite; in that case, deleting an older offset will return 404 Not Found.
3. Bootstrap
Bootstrap provides single-request initialization: a snapshot (if any) plus all retained updates after the snapshot point, returned as a single ordered response that preserves per-message content types.
3.1. Request
GET {stream_url}/bootstrap
Query Parameters:
/bootstrapis a one-shot initialization endpoint. It does not define any query parameters of its own.- Servers SHOULD reject any
livequery parameter on/bootstrapwith400 Bad Request.
3.2. Response
Response Headers:
Content-Type: multipart/mixed; boundary=<token>Stream-Snapshot-Offset: The snapshot offset, or-1if no snapshot exists.Stream-Next-Offset: The next offset after all returned data.Stream-Up-To-Date: Boolean.
Response Body:
The body is an RFC 2046 multipart/mixed entity. Each MIME part is one logical bootstrap message. The multipart boundary, not any outer binary framing, defines the bootstrap message boundaries.
- First part: Snapshot message. If a snapshot exists, the part body MUST be the raw snapshot bytes and the part
Content-TypeMUST equal the snapshot blob's stored content type. - Subsequent parts: Retained updates after the snapshot point, one update message per part. Each part body MUST be exactly one retained update message, and the part
Content-TypeMUST be the content type of that update. For streams with a fixed stream-level content type, all update parts will normally share that value.
If no snapshot exists, the server MUST still emit an empty first part, Stream-Snapshot-Offset MUST be -1, and that empty part's Content-Type MUST be application/octet-stream. Update parts then begin at the earliest retained offset.
Servers MUST NOT wrap the entire bootstrap response in an outer binary framing container. Each update part contains exactly one retained update message from the stream. Implementations MUST preserve the same retained-message boundaries they use for ordinary reads and snapshot-offset validation, and they MUST NOT reinterpret or reframe binary payloads when constructing bootstrap parts.
Example:
HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=rr-bootstrap-9f1c2e7a
Stream-Snapshot-Offset: 0000000000000012
Stream-Next-Offset: 0000000000000026
Stream-Up-To-Date: true
Cache-Control: no-store
--rr-bootstrap-9f1c2e7a
Content-Type: application/octet-stream
<snapshot-bytes>
--rr-bootstrap-9f1c2e7a
Content-Type: application/json
{"op":"set","path":["title"],"value":"hello"}
--rr-bootstrap-9f1c2e7a
Content-Type: application/json
{"op":"insert","path":["body",0],"value":"world"}
--rr-bootstrap-9f1c2e7a--
Response Codes:
200 OK: Bootstrap data returned.404 Not Found: Stream does not exist.
3.3. Follow-Up Live Reads
Bootstrap is one-shot only:
/bootstrapreturns an ordered initialization payload once./bootstrapdoes not supportlive=sseorlive=long-poll.- After applying the bootstrap response, clients MUST continue tailing through the ordinary stream read APIs using the returned
Stream-Next-Offset. - Clients SHOULD prefer
GET {stream_url}?offset=<offset>&live=sseand fall back toGET {stream_url}?offset=<offset>&live=long-pollwhen SSE is unavailable.
3.4. Compatibility
- Regular
GET {stream_url}?offset=...MUST NOT return snapshot bytes. Snapshots are only delivered through/bootstrap. - When a server returns
410 Gonefor a read (because retention has advanced), clients SHOULD call/bootstrapto rebuild state, then continue tailing from the returnedStream-Next-Offsetthrough the ordinary read APIs. - If a server has begun returning
410 Gonefor a stream (i.e., earliest retained offset >-1), the server MUST ensure/bootstrapis available and can provide a snapshot covering that earliest retained offset. - Clients MUST parse
/bootstrapas an ordered message sequence. Message boundaries come from MIME parts.
4. Ordinary SSE Compatibility
Ordinary SSE behavior, including binary stream handling, is defined by docs/specs/durable-stream.md.
- For binary streams, ordinary
event: datapayloads are raw base64 text and the response MUST includestream-sse-data-encoding: base64. - Extension endpoints MUST NOT redefine the payload shape of ordinary binary SSE
event: dataframes unless they document an explicit endpoint-local contract.
5. URL Encoding
snapshot_offset values originate from Stream-Next-Offset and MAY contain characters requiring URL encoding. Clients and servers MUST apply standard URL encoding/decoding when using offset values in path segments.