chroma-core/chroma · error · ValueError
conditional transaction cannot span collections
Error message
conditional transaction cannot span collections
What it means
This ValueError comes from ConditionalHttpTransaction._record_scope: the first buffered operation in a conditional transaction fixes its scope (collection id + tenant + database), and any subsequent operation against a different collection, tenant, or database raises 'conditional transaction cannot span collections'. A conditional HTTP transaction validates reads/writes against one collection's state, so it cannot batch work for multiple collections.
Source
Thrown at chromadb/api/conditional_http.py:228
scope = self._require_scope()
return (scope, prepared_commit)
def close(self, first_inserted_record_offset: Optional[int] = None) -> None:
self._ensure_open()
self._closed = True
def _ensure_open(self) -> None:
if self._closed:
raise ValueError("conditional transaction is closed")
def _record_scope(
self, collection_id: UUID, tenant: str, database: str
) -> ConditionalHttpScope:
scope = ConditionalHttpScope(str(collection_id), tenant, database)
if self._scope is None:
self._scope = scope
elif self._scope != scope:
raise ValueError("conditional transaction cannot span collections")
return scope
def _require_scope(self) -> ConditionalHttpScope:
if self._scope is None:
raise ValueError("conditional transaction has no collection scope")
return self._scope
def _validate_get_request(self, request_payload: ConditionalHttpGetPayload) -> None:
ids = request_payload.get("ids")
if ids is not None:
for id in ids:
if id in self._buffered_write_ids:
raise _invalid_read_after_write(id)
return
limit = request_payload.get("limit")
if not isinstance(limit, int) or limit <= 0:
raise InvalidArgumentError(View on GitHub (pinned to aecdd12c8a)
Solutions
- Use one transaction per collection: commit/close the current one and open a new transaction before touching another collection (or tenant/database)
- If you need cross-collection atomicity, note the HTTP conditional transaction does not provide it — restructure to per-collection transactions with your own compensation, or use the local PersistentClient with SQLite transactions where applicable
- Record the scope (collection id/tenant/database) alongside the transaction in your wrapper and assert it matches before each buffered call
Example fix
# before tx = client.create_conditional_transaction() buffer_add(tx, coll_a, ids=['1'], embeddings=[[0.1]]) buffer_add(tx, coll_b, ids=['2'], embeddings=[[0.2]]) # ValueError: cannot span collections # after tx1 = client.create_conditional_transaction() buffer_add(tx1, coll_a, ids=['1'], embeddings=[[0.1]]) commit(tx1) tx2 = client.create_conditional_transaction() # separate transaction per collection buffer_add(tx2, coll_b, ids=['2'], embeddings=[[0.2]]) commit(tx2)
Defensive patterns
Strategy: validation
Validate before calling
def buffer_add_scoped(tx, scope_key, collection, **kwargs):
"""scope_key: (collection_id, tenant, database) the tx was opened for"""
current = (str(collection.id), collection.tenant, collection.database)
if tx._scope is not None and tuple(tx._scope) != current:
raise ValueError(
f'transaction is scoped to {tx._scope}; commit it and open a new one for {current}'
)
return buffer_add(tx, collection, **kwargs) Type guard
def same_scope(tx, collection_id, tenant, database) -> bool:
s = getattr(tx, '_scope', None)
return s is None or (s.collection_id == str(collection_id) and s.tenant == tenant and s.database == database) Try / catch
try:
buffered_op(tx, coll_b, ...)
except ValueError as e:
if 'cannot span collections' in str(e):
commit(tx)
tx = client.create_conditional_transaction()
buffered_op(tx, coll_b, ...)
else:
raise Prevention
- One transaction per (collection, tenant, database); commit before switching
- Group work by collection before opening transactions so each tx has a single target
- Do not change tenant/database mid-transaction; open a new transaction after switching
- If you need multi-collection atomicity, design compensation logic — HTTP conditional transactions will not provide it
When it happens
Trigger: Opening a conditional transaction, buffering an add/update/delete or issuing a get against collection A, then buffering another operation against collection B with the same transaction object. Also triggered by switching tenant or database between operations inside one transaction (the scope is a (collection_id, tenant, database) triple).
Common situations: Batch pipelines that fan writes out to multiple collections inside one 'transaction' for atomicity; multi-tenant code that changes tenant/database mid-transaction; generic helper functions that accept a transaction and a collection and are called in a loop over collections.
Related errors
- conditional transaction is closed
- txn.commit() cannot be called inside run()
- Conditional transactions are only supported when connecting
- Embedding function provided when already defined in the coll
- Aggregate input must be an Aggregate instance or object with
AI-assisted analysis of chroma-core/chroma@aecdd12c8a (2026-08-16).
Data as JSON: /api/errors/ce24b46e1409c4f0.
Report an issue: GitHub.