What a million vectors costs to keep and to query
Two different costs: per-call for upsert and query, plus standing rent while the collection exists. Which dominates depends on your read-to-write ratio.
The reason to run retrieval on Infrai isn’t the rate — it’s that the embedding model, the index, the model that writes the answer and the scrape that fetched the source are one credential. A RAG pipeline is four vendors by default, and reconciling four invoices to answer “what does retrieval cost us” is the work this removes.
The cost itself has two parts that behave differently, and knowing which dominates for your shape is the whole exercise.
Per-call, and standing
curl -sS "https://api.infrai.cc/v1/discovery/vector.query" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"id": "vector.query",
"method": "POST",
"path": "/v1/vector/query",
"minimum_tier": "standard",
"self_hosted": true,
"billing": {
"is_billable": true,
"unit": "per_call",
"price_usd": 0.0002,
"currency": "USD",
"approximate": false
}
}
Query is per call and upsert is per call, both fractions of a cent and both readable live from GET /v1/discovery/{capability} — verified 2026-09-21, approximate: false.
The other half is rent. A collection is a real index occupying real storage, so it accrues cost by occupied gigabyte over time the way stored objects do. That’s the part that grows while you’re not looking, and for a million vectors it’s the part that matters.
Which dominates
| Shape | Dominant cost |
|---|---|
| Large index, few queries (internal archive) | standing rent |
| Small index, heavy queries (a chatbot) | per-call queries |
| One-off bulk index, steady reads | rent, after the first month |
| Continuous re-indexing | upsert calls plus rent |
| Collection nobody queries any more | rent, entirely wasted |
The last row is where money quietly goes.
An abandoned experiment’s collection keeps paying rent for as long as the account exists, because nothing on the platform decides that an index has stopped being useful — and the person who created it has usually moved on to something else, so the collection outlives everyone’s memory of what it was for. That is the single most common line item on a retrieval bill that nobody can explain.
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}"})
def rate(capability: str) -> float:
resp = SESSION.get(f"{API}/v1/discovery/{capability}", timeout=20)
resp.raise_for_status()
return float((resp.json().get("billing") or {}).get("price_usd") or 0.0)
def collections() -> list[dict]:
resp = SESSION.get(f"{API}/v1/vector/collection/list", timeout=30)
resp.raise_for_status()
return resp.json()["data"].get("collections") or []
def inventory() -> list[dict]:
"""Every collection with its size. An index nobody queries still pays rent, so
this listing is the first place to look before optimising anything else."""
out = []
for entry in collections():
name = entry if isinstance(entry, str) else entry.get("collection")
detail = SESSION.get(f"{API}/v1/vector/collection/get",
params={"collection": name}, timeout=30)
data = detail.json().get("data", {}) if detail.ok else {}
out.append({"collection": name, "vectors": data.get("vector_count"),
"dimension": data.get("dimension"), "metric": data.get("metric")})
return sorted(out, key=lambda row: -(row["vectors"] or 0))
def spend() -> dict:
resp = SESSION.get(f"{API}/v1/account/usage", timeout=25)
resp.raise_for_status()
data = resp.json()["data"]
rows = {r["key"]: r for r in data.get("breakdown", []) if r["key"].startswith("vector.")}
return {"period": data.get("period"),
"vector_total": round(sum(r["cost"] for r in rows.values()), 4),
"by_operation": {k: {"cost": round(v["cost"], 4), "calls": v["calls"]}
for k, v in rows.items()},
"query_rate": rate("vector.query"), "upsert_rate": rate("vector.upsert"),
"account_total": data.get("total_cost")}
if __name__ == "__main__":
for row in inventory():
print(f"{row['collection']:<28} {row['vectors']} vectors, dim {row['dimension']}")
print(spend())
Run the inventory first. The answer to “why is retrieval expensive” is usually a collection with two million vectors from a re-index that ran twice.
Four levers, in order of effect
Delete collections you stopped using. DELETE /v1/vector/collection/delete is one call and it removes a recurring cost entirely. This is the largest lever and the one nobody pulls.
Stop duplicating on re-index. Deterministic ids mean an id that already exists is overwritten rather than added. Random ids mean a re-index doubles your vector count, and your rent, silently.
Chunk less aggressively. A 600-character chunk size produces twice the vectors of a 1,200-character one for the same corpus. Retrieval quality is not proportional to chunk count, so measure before assuming smaller is better.
Batch your upserts. One call with a hundred vectors rather than a hundred calls — the rate is per call, not per vector.
The read-to-write ratio decides your architecture
For a chatbot answering a thousand questions a day against a modest index, queries dominate and the index size barely matters. For an archive of ten million documents queried twice a week, rent is essentially the entire bill and query optimisation is pointless.
Work out which you are before tuning anything. GET /v1/account/usage shows the split between vector.query and vector.upsert calls, and the rent shows up alongside them — one read, no estimation.
Structural facts worth planning on
Three things survive any repricing. There’s no plan minimum and no pod or node to provision, so a small index costs proportionally little rather than a monthly floor — which is the specific difference from managed vector databases that price per node. Deleting a collection stops its cost immediately. And every new account starts with $2, which at these per-call rates covers a lot of experimentation.
One operational fact to know: metered resources belong to an account in good standing, so an index is not the place to keep the only copy of anything you can’t regenerate. Keep the source documents in object storage and treat the index as derived.
Limitations
There’s no per-collection cost line: GET /v1/account/usage breaks down by capability, so attributing rent to a specific collection means inferring it from vector_count and dimension rather than reading it. And there’s no index tuning — no choice of index type, no parameters to trade recall for memory — so you can’t optimise storage the way you could on self-managed Qdrant.
pgvector on a Postgres instance you already run is effectively free at small scale, and Qdrant Cloud is cheap per node at large scale with far more index control — both are honest alternatives if vector search is the substance of your product rather than one step in a pipeline. Rates here drift downward as vendor contracts improve, so read them from your own account rather than this page.