Skip to main content

A publish acknowledges durability, not visibility

A 200 from POST /v1/publish means the message is on durable media and survives a restart. It becomes searchable shortly afterwards. There is no read-your-writes guarantee and no “wait until indexed” flag. A search that returns {"results": []} immediately after a publish is expected behaviour, not a failure. Run it again in a moment.

Acknowledgement modes

ack takes stored (the default) or durable. Both acknowledge only after the message is written to durable storage, so choosing between them changes nothing today. The field is accepted so a future deployment can offer a weaker, faster mode without a breaking change.

Making a retry safe

A publish that times out may or may not have been stored. idempotency_key closes that gap:
It is a body field, not an HTTP header. Maximum 256 bytes, valid UTF-8, no control characters. The key is scoped to the namespace, so a retry is still recognised even when its embedding differs slightly from the first attempt.
The retry window is best-effort and does not survive a server restart. A retry sent across one may be stored a second time. Use idempotency_key to make a retry safe, not as a long-lived deduplication guarantee.
On a 409, retrying as-is will keep failing. Either re-send the original message under that key, or publish the new message under a key of its own.

Identifying and ordering messages

A publish returns three values:
message_id is stable and globally unique. It is the same identifier that comes back in search results and in match frames, and it is what you deduplicate on. epoch and seq are opaque. Consecutive publishes to one namespace routinely return unrelated and non-increasing values, so seq is not a namespace-wide ordering. Do not use it to order messages, to detect gaps, or as a cursor. It is not a position you can pass to POST /v1/search. Order by publish-time metadata you attach yourself.

What a subscriber has to handle

Matches are delivered at-least-once. A consumer can see the same message twice, so deduplicate on message_id if a repeat would be harmful. POST /v1/subscribe answers with a Server-Sent Events stream. The first frame confirms registration:
Each match after that carries identifiers, not bodies. Read the body with POST /v1/search when you need it:
Rely on message_id and score. Treat anything else in the frame as opaque. The stream also carries keepalive comment frames — lines beginning with : — so an idle subscription is distinguishable from a dead one. Ignore them and never surface them as matches. Use a real SSE parser rather than splitting on newlines; comment-only lines and multi-line data: fields are where hand-rolled parsers break.
A subscription matches only messages published while it is open. It does not replay history. Pair it with a search if you also need what came before.

Setup failures and stream failures are different

A subscribe call that fails before the stream opens committed no state. Nothing was installed and no match can have been missed, so retrying the open is safe. Treat a transient code here exactly as you would on publish or search: honour retry_after_ms and reopen. See Errors and retries. A stream that dies after delivering matches is a different problem. There is no resume cursor. Reconnecting registers a logically fresh subscription, and whether you see a replay or a gap across the reconnect is not guaranteed. Surface the disconnect to the caller with the last message_id seen, deduplicate on reconnect, and let the caller decide whether to reopen rather than reconnecting automatically.

Timeouts

Bound the wait for the first frame. A few seconds is enough, and exceeding it means setup failed. Leave the read side long or unbounded: a healthy subscription can be quiet for a long time, and the keepalive frames are what tell you it is still alive.