Home / AI & GenAI / Vector Databases

AI & GenAITroubleshootingIntermediate

Pinecone query doesn't return records you just upserted: freshness, eventual consistency and how to design around it

A user uploads a document and asks about it immediately, but the assistant can't find it. Tests that upsert then query fail randomly. Understand Pinecone's eventual consistency, tell real ingestion failures apart from freshness lag, and make the application and tests behave correctly.

Deepak Bhardwaj5 min read

Applies toPinecone serverless indexes, Python SDK; the patterns apply to most managed vector databases with eventually consistent reads

Symptoms

  • "Upload a file and ask about it" works most of the time, but sometimes the first question returns nothing relevant.
  • Integration tests that upsert and immediately query are flaky.
  • describe_index_stats() shows fewer records than were just written, then catches up.
  • fetch(ids=[...]) finds a record that query doesn't return yet.

Why it happens

Pinecone serverless indexes are eventually consistent. An upsert returns success once the write is accepted durably, but the record becomes visible to queries a little later, after it has been indexed. Under normal conditions that delay is short (usually seconds), but it isn't zero and can grow during heavy write load. Stats counts are also computed asynchronously.

This isn't a bug to fix in Pinecone; it's a property to design for.

Step 1: separate lag from failure

Before blaming freshness, make sure the write actually happened:

Check How If it fails
Upsert call succeeded Log the response; exceptions caught? Fix error handling; see 429/size errors
Record exists index.fetch(ids=["doc42#0"], namespace=ns) Wrong namespace, wrong ID, or write never sent
Same namespace for write and read Log the namespace on both paths Fix routing; check for default "" namespace
Same index host Log host on both paths Writer and reader point at different indexes (e.g. staging vs prod)
Record matches the filter Check metadata in the fetch result Filter excludes it (type mismatch, case)
Record appears after 30–60 s Query again later If never, it's not freshness

Many "freshness" incidents turn out to be a namespace mismatch or a filter that excludes the new record, for example year stored as the string "2026" while the filter uses a number.

Step 2: measure the actual lag

Run a small probe that writes a marker record and polls until a query returns it:

import time, uuid

def freshness_probe(index, ns, vec):
    rid = f"probe-{uuid.uuid4()}"
    index.upsert(vectors=[{"id": rid, "values": vec, "metadata": {"probe": True}}], namespace=ns)
    start = time.time()
    while time.time() - start < 120:
        res = index.query(vector=vec, top_k=1, namespace=ns, filter={"probe": {"$eq": True}})
        if res.matches and res.matches[0].id == rid:
            lag = time.time() - start
            index.delete(ids=[rid], namespace=ns)
            return lag
        time.sleep(0.5)
    return None

Run it every few minutes into a dedicated probe namespace and chart the result. It gives you a real number to put in the design, and an alert when lag grows during bulk loads.

Step 3: design the user experience

Pattern How
Show ingestion status "Processing… ready in a few seconds"; enable "ask about this file" when ready
Wait for visibility before marking ready After upsert, poll with fetch + a filtered query on the new doc_id until it returns, with a timeout
Answer from the uploaded file directly For the first question on a just-uploaded file, pass its chunks straight to the LLM without retrieval
Read-your-writes cache Keep the last N uploaded chunks per session in the app and merge them into retrieval results

The direct-context pattern is simple and reliable: the user just uploaded the file, you already have its text, and you don't need search to find it.

Step 4: fix flaky tests

Don't add sleep(5) and hope. Poll with a deadline:

def wait_until_queryable(index, ns, doc_id, vec, timeout=60):
    deadline = time.time() + timeout
    while time.time() < deadline:
        res = index.query(vector=vec, top_k=1, namespace=ns, filter={"doc_id": {"$eq": doc_id}})
        if res.matches:
            return
        time.sleep(0.5)
    raise TimeoutError(f"{doc_id} not queryable after {timeout}s")

Also give each test run its own namespace (test-{run_id}) and delete it at the end, so runs don't see each other's data.

Step 5: updates and deletes too

The same delay applies to:

  • Updates: a query may briefly return the old metadata, which matters for permission changes. If a user loses access to a document, enforce the change in your application (check permissions on returned doc_ids against your source of truth) instead of relying only on the metadata filter.
  • Deletes: a deleted document can show up in results for a short time. Post-filter results against a "deleted documents" set in the application during that window if that's unacceptable.

Important

For security-relevant changes (access revoked, legal deletion), never rely on vector index freshness alone. Check returned documents against the authoritative permission store before showing them.

Step 6: watch stats correctly

describe_index_stats() counts trail writes. Don't use them as an exact "ingestion finished" signal right after a load. Use your loader's own count of successful upserts, then compare with stats after a pause, and spot-check with fetch.

Decision table

Finding Action
Record never appears Not freshness: check namespace, host, ID, filter, errors
Appears within seconds Design UX/test around it
Lag grows to minutes during bulk loads Throttle loader, load into a new index, or schedule loads off-peak
Permission change visible late Enforce permissions in the application too

Prevent it next time

  • Treat "written" and "searchable" as two states in your ingestion pipeline.
  • Run a freshness probe and alert on lag.
  • Use poll-with-deadline in tests, never fixed sleeps.
  • Check permissions on results against the source of truth.