Tether

stored, searchable, and degraded

This is the guide integrators most need and most often skip. It is about one fact: a write that Tether accepted is not immediately findable by search, and what to do about it.

Every number and response below was measured against real Workers AI and real Vectorize, not estimated.

The three signals

Signal Where Means
stored: true write responses durably committed to D1
searchable: false write responses not yet in the vector index
degraded: true /search, /context this space has writes still waiting to be handed to the index
via: "recent" /context anchors this anchor is one of them

The window, measured

Writing an anchor enqueues its indexing work. A cron drains that queue, embeds the content, and upserts a vector. Two delays stack:

Stage Measured
Write → drain claims it up to 60s (one-minute cron floor)
Upsert accepted → actually queryable p50 28.8s, p95 44.5s

So an accepted write can be unsearchable for roughly 105 seconds in the worst case. In a capture for these guides the observed end-to-end time from POST /v1/anchors to a hit in /v1/search was 23.2 seconds, with the drain triggered manually.

Note the second row carefully: Vectorize acknowledges an upsert long before the vector is queryable. A successful upsert does not mean "searchable now," and nothing in Tether treats it that way.

What this looks like

Immediately after writing two anchors, with no drain in between — same credential, same instant, both endpoints:

POST /v1/search

{ "results": [], "degraded": true, "requestId": "req_d654aeb7-…" }

POST /v1/context

{
  "anchors": [
    { "id": "anc_…E1K8CA", "content": "Ontra prefers concise summaries with no preamble.",
      "score": null, "via": "recent", "…": "…" },
    { "id": "anc_…DQFS65", "content": "We deploy on Thursdays after the release review.",
      "score": null, "via": "recent", "…": "…" }
  ],
  "evidence": [],
  "graphRevision": "1789870850497",
  "tokenCount": 33,
  "tokenizer": "tether-upper-bound-v1",
  "degraded": true
}

Same data, same moment. /v1/search finds nothing because there is no vector to match. /v1/context reads the anchors straight out of D1 and labels them via: "recent" with score: null.

After the drain and the index catching up, the same /v1/context query returns them via: "query" with real scores, and degraded is false.

degraded: false does not mean "searchable"

This is the sharp edge, and the one measurement that surprised us.

degraded counts one thing: outbox rows still waiting to be drained. The drain marks a row done when Vectorize accepts the upsert — and Vectorize applies upserts asynchronously, returning a mutationId rather than a promise that the vector is live. The second row of the table above is that gap, and degraded does not cover it.

Measured against the deployed Worker, two anchors written and then polled:

degraded flips true → false t+59s
/v1/search first returns them t+80s

A 21-second window where degraded is false and the write is still not findable. So:

Neither value is a signal to poll on. If you need a specific write to be visible, read it by id — GET /v1/anchors/{id} is immediately consistent — or retrieve through /v1/context, whose read-through covers both halves of the window, not just the first.

What to do

Retrieve through /v1/context, and you mostly do not need to care. The read-through covers the window. This is why /v1/context is the endpoint to build an agent on.

Do not poll after a write. It is the instinct this guide exists to prevent. You will burn your rate limit — 120 requests per 60 seconds, per credential — and get nothing that /v1/context would not have given you immediately.

Do not treat degraded: true as an error. It is not a failure. The results are valid; they may be incomplete. Show them.

Do surface it if completeness matters. For an agent answering from memory, degraded: true is worth a hedge — "based on what I have indexed so far."

Do not assume via: "recent" means relevant. Those anchors were not matched, they were included because they cannot be matched yet. An unrelated note written a minute ago will appear. They are capped at five, ranked after genuine matches, and carry score: null so you can always tell them apart.

If it stays degraded

degraded: true that never clears means the queue is not draining. Likely causes, in order:

  1. The scheduled worker is not deployed or its cron is not registered. A deployment with no scheduled handler accepts writes and never indexes them.
  2. Embeddings or the index are failing. Rows retry with exponential backoff and are parked as failed after five attempts rather than blocking the queue behind them.
  3. A genuine backlog. The drain claims a bounded batch per minute.

A permanently failed row means an anchor is stored and will never be searchable. It is still readable by id, and still returned by /v1/context only while inside the read-through window — after that it is invisible to retrieval. That is a real failure mode and worth alerting on.

Deletion has the same shape

DELETE /v1/anchors/:id returns 202, not 204, for exactly the same reason: D1 rows are removed synchronously, vectors asynchronously. Poll GET /v1/deletions/:id until stages.vectors is done. Guide 03 covers it.

The asymmetry is deliberate. A write that is not yet indexed is invisible to search, which is safe. A delete that is not yet applied would leave content findable, which is not — so the read path refuses deleted content from D1 immediately, regardless of what the index still holds.