How Bulk Ingest Behaves¶
This page explains what happens when you hand the SDK a large amount of data
in one call, and what each knob controls. The batched calls are
documents.batch_upsert() for a document index, and upsert(vectors=..., batch_size=...) or upsert_from_dataframe() for an index created with
top-level dimension=/metric=. For transport and throughput tuning, see
Performance; for per-request retry settings, see
Retries; for DataFrame recipes, see
Reliable Large Ingests with upsert_from_dataframe.
The REST sync, asyncio and gRPC clients share one bulk engine, so everything here applies to all three. Vectors and documents name things differently:
Vectors |
Documents |
|
|---|---|---|
Batched call |
|
|
Returns |
|
|
Accepted-item count |
|
|
Default |
unbatched for |
50; the server accepts at most 1000 per request |
documents.upsert() is a single request, so nothing on this page applies to it.
How a batched call runs¶
The SDK splits your input into batches of batch_size items. A per-host
admission gate decides how many batches may be in flight at once; this is
where backpressure lives. Each admitted batch is one request with its own
automatic retries. When every batch has settled, the call returns one result
that counts what landed and lists what failed. Per-batch failures are
reported, not raised.
Why your ingest slows down sometimes¶
The gate keeps a concurrency limit for each host that adapts to what the backend can absorb:
When the server signals overload (HTTP 429, or a gRPC “slow down”), the limit drops sharply. If the server says to retry after some interval, the gate holds every new batch until that interval passes.
When batches succeed steadily, the limit recovers one step at a time.
A slowdown mid-ingest usually means the SDK heard the backend ask for room and gave it. The ingest finishes sooner than it would by hammering a server that is shedding load, and other clients of the same index stay healthy.
The gate is shared by every bulk call to the same host in the process, so
two Index handles pointed at the same index cannot double the load on it.
Retries are budgeted per host as well: once retryable failures outnumber
successes by enough, the SDK stops retrying, so retries switch themselves off
during a full outage instead of multiplying load.
The knobs¶
batch_size¶
How many items go in one request. Defaults are in the table above.
Bigger batches mean fewer but heavier requests, and a larger unit of failure (a batch succeeds or fails as a whole). The ceiling is the server’s request size limit, which a batch of large items reaches sooner than the item count suggests. Lower it when your items are large; raise it when they are tiny.
max_concurrency¶
The most batches this call keeps in flight at once. Default 8 on every method and transport.
This is a cap, not a target: the gate can hold effective concurrency below it while the backend pushes back. Raising it raises the best case, and also the retry burst you commit to during an outage. See Tuning max_concurrency.
total_timeout¶
A deadline in seconds for the whole call. timeout, by contrast, bounds one
attempt of one batch. Default None: no wall-clock bound.
When the deadline passes, the SDK stops submitting batches. Batches already
in flight finish and are never cancelled, so total_timeout never tears down
work the server may already be applying. Everything unsent comes back as
failed items you can retry.
There is no default deadline because a slow but progressing ingest should be
allowed to finish; a dead backend is caught by the stall detector (below),
which watches progress rather than the clock. Set total_timeout when your
job has a real external deadline, such as a cron slot, and size it
generously.
timeout¶
The per-attempt timeout for one batch request. A batch that times out is
retried like any other transient failure, so one batch can take several
multiples of timeout, plus backoff, before it is reported failed. Raise it
for very large batches on slow networks; to bound the whole ingest, use
total_timeout.
When the backend is actually down: the stall detector¶
If the gate has dropped to its floor and several consecutive batches then fail outright (every retry exhausted, no successes), the SDK concludes the backend is unavailable and abandons the rest of the call instead of grinding through every remaining batch:
The result carries the unsent work in
failed_items, and the abandonment errors saybackend appears unavailable.A warning is logged with how many batches were abandoned.
For a short cool-down, further bulk calls to the same host fail fast the same way. After it, the SDK probes again and recovers on the first success.
Treat it like any partial failure: inspect the result, wait or alert, and retry the failed items later (pattern below). The detector never trips on a backend that is slow but working.
Partial failures: the reporting contract¶
Batched calls do not raise on per-batch failures. The result tells you what happened:
upserted_count(vectors) orsuccessful_item_count(documents): items the server accepted.failed_item_countandfailed_items: exactly what did not land, ready to feed back into a retry call.errors: one entry per failed batch (several can share abatch_indexwhendocuments.batch_upsertsplit an oversize batch and only some pieces failed). Each carries the items, the error, aretryablehint, and adispositionsaying how it failed:rejected: the attempt completed with an error, after retries. The write may still have landed, because a response can be lost after the server applied it.unsent: never attempted becausetotal_timeoutpassed first.abandoned: never attempted because the stall detector fired.
Dispositions are an open set. Branch on the values you handle and send any other value to your default path.
upsert_from_dataframe accepts on_error="raise" if you would rather have
the lowest-indexed batch failure re-raised after all batches settle; the
partial result is on the exception’s response attribute.
Reading the gate counters on the result¶
documents.batch_upsert() also reports what the admission gate did during the
call. The gate is shared by every bulk call to the same host in the process.
Field |
Meaning |
How to read it |
|---|---|---|
|
Throttle signals (HTTP 429, gRPC “slow down”) the host’s gate saw during the call |
Includes throttles from concurrent calls to the same host. Nonzero means the backend pushed back, not that the call failed. |
|
Most batches this call had in flight at once |
Never exceeds |
|
The host gate’s concurrency limit when the call finished, from 1 to 64; |
Compare with 64, not |
|
|
Abandoned batches are in |
Retrying safely¶
Upserts are idempotent by id: re-sending an item that already landed
overwrites it with the same data, so retrying failed_items is safe.
Retrying in an unbounded loop is not, because an unhealthy backend keeps it
spinning forever. Bound the attempts, keep only the items whose batch is
marked retryable, and back off between rounds:
import time
from pinecone import Pinecone
pc = Pinecone()
index = pc.index(name="product-catalog")
result = index.documents.batch_upsert(namespace="products-en", documents=documents)
for attempt in range(3):
if not result.has_errors:
break
retryable = [item for err in result.errors if err.retryable for item in err.items]
if not retryable:
break
time.sleep(2**attempt)
result = index.documents.batch_upsert(namespace="products-en", documents=retryable)
For vectors, call index.upsert(vectors=..., batch_size=...) in both places;
UpsertResponse has the same has_errors and errors.
A batch marked retryable=False was rejected for a reason a retry cannot
fix, such as malformed data, a dimension mismatch or bad credentials.
Re-sending it only spends your retry budget; log it and move on.
What you don’t have to manage¶
Thundering herds. Retry delays are jittered, and server
Retry-Afterhints are spread out, so many clients told to come back at the same moment do not all return at once.Coordination within one process. The gate and the retry budget are shared per host, so concurrent bulk calls share capacity instead of competing for it.
Transport differences. REST sync, asyncio and gRPC run the same engine with the same knobs and the same reporting contract. Choose a transport on its own merits (Sync vs Async, gRPC), not for bulk behavior.