Home / AI & GenAI / Vector Databases

AI & GenAITroubleshootingIntermediate

Pinecone upserts fail with 429 Too Many Requests or crawl along: batch sizing, parallel upserts, retries and bulk import

Loading a few million embeddings into Pinecone takes all day, requests fail with 429 or "request size exceeds limit", and retries make it worse. Right-size batches, run upserts in parallel within the limits, retry with backoff, and use bulk import from object storage for big loads.

Deepak Bhardwaj5 min read

Applies toPinecone serverless indexes, Python SDK (pinecone package, REST and gRPC clients); limits change by plan, so check your project's current quotas

Symptoms

  • Ingestion jobs log 429 Too Many Requests or RESOURCE_EXHAUSTED (gRPC).
  • 400 Bad Request with a message about request or message size.
  • A loader that upserts one record per request runs for hours.
  • Retries pile up and the job never catches up with new documents.

Step 1: find which limit you hit

Error Limit Fix
Request size / message too large Per-request size (about 2 MB) and record count (1,000 per upsert) Smaller batches; trim metadata
Metadata too large for record Metadata per record (40 KB) Don't store full chunk text in metadata if it's huge; store a pointer
429 on writes Write throughput limits per index/namespace and plan Fewer parallel workers, backoff, or bulk import
429 on reads during load Read limits; load competes with live queries Run loads off-peak or into a separate index, then switch

Note

Pinecone publishes its limits per plan and changes them over time. Read the current "Limits" page for your plan instead of hard-coding numbers from a blog post.

Log the full response body; it usually names the exact limit.

Step 2: batch properly

One record per request wastes most of the time on network round-trips. Batch by count and size:

from pinecone import Pinecone
import json

pc = Pinecone(api_key=API_KEY)
index = pc.Index(host=INDEX_HOST)          # use the host from describe_index, not a name lookup per call

MAX_RECORDS = 200
MAX_BYTES = 1_500_000                       # stay under the request size limit with margin

def batches(records):
    batch, size = [], 0
    for r in records:
        r_size = len(json.dumps(r).encode())
        if batch and (len(batch) >= MAX_RECORDS or size + r_size > MAX_BYTES):
            yield batch
            batch, size = [], 0
        batch.append(r)
        size += r_size
    if batch:
        yield batch

for b in batches(records):                 # records: [{"id":..., "values":[...], "metadata":{...}}]
    index.upsert(vectors=b, namespace="kb-prod")

With 1536-dimension float vectors, each record is roughly 6 KB of values as binary but much more as JSON, so the size limit, not the 1,000-record limit, is usually the one that applies. The gRPC client (pip install "pinecone[grpc]") sends vectors more compactly than REST JSON.

Step 3: parallelise, within limits

from pinecone.grpc import PineconeGRPC

pc = PineconeGRPC(api_key=API_KEY)
index = pc.Index(host=INDEX_HOST)

futures = [index.upsert(vectors=b, namespace="kb-prod", async_req=True) for b in batches(records)]
for f in futures:
    f.result()                              # raises if a batch failed

Start with a small number of concurrent requests (4–8), measure records per second and 429 rate, then raise slowly. More parallelism past the throughput limit only produces more 429s.

Step 4: retry with backoff and jitter

import random, time

def upsert_with_retry(batch, attempts=6):
    for i in range(attempts):
        try:
            return index.upsert(vectors=batch, namespace="kb-prod")
        except Exception as e:
            if "429" not in str(e) and "RESOURCE_EXHAUSTED" not in str(e) and i > 0:
                raise
            time.sleep(min(30, (2 ** i)) + random.random())
    raise RuntimeError("upsert failed after retries")

Upserts are idempotent by ID: re-sending the same batch overwrites the same records. That makes retries safe, as long as IDs are deterministic (e.g. doc_id#chunk_no, not random UUIDs generated per attempt).

Step 5: use bulk import for large initial loads

For millions of records, serverless indexes support importing Parquet files from object storage (S3, and other providers depending on current support). It runs as an asynchronous job on Pinecone's side and doesn't consume your upsert request quota in the same way.

op = index.start_import(
    uri="s3://my-bucket/embeddings/kb-prod/",
    integration_id=STORAGE_INTEGRATION_ID,  # storage integration configured in the console
    error_mode="CONTINUE",
)
print(index.describe_import(id=op.id))

Prepare files in the documented Parquet schema (id, values, optional metadata as JSON), with one directory per namespace. Check the import documentation for the exact layout and size limits before generating files.

Load size Approach
Up to tens of thousands of records Batched upserts
Hundreds of thousands to a few million, recurring Parallel gRPC upserts with backoff
Initial load or full re-embed of millions Bulk import from object storage

Step 6: don't fight live traffic

If the index also serves production queries, a full reload competes with them. Options:

  • Load into a new index, verify counts and recall, then switch the application's index host.
  • Throttle the loader during business hours.
  • Use a separate namespace and switch the namespace name in config after the load.

Step 7: verify the load

stats = index.describe_index_stats()
print(stats.total_vector_count, stats.namespaces.get("kb-prod"))

Counts are eventually consistent and can trail recent writes briefly. Compare with your source count after the loader finishes and a short wait, and spot-check with index.fetch(ids=[...], namespace="kb-prod").

Prevent it next time

  • Use deterministic record IDs so every retry and re-run is safe.
  • Keep metadata small: filterable fields only, plus a pointer to the chunk text if it's large.
  • Put batch size, concurrency and backoff in configuration, not code.
  • Use bulk import for any load over a million records.
  • Alert on 429 rate in the ingestion job, not only on job failure.