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

  1. Use one transaction per collection: commit/close the current one and open a new transaction before touching another collection (or tenant/database)
  2. 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
  3. 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

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


AI-assisted analysis of chroma-core/chroma@aecdd12c8a (2026-08-16). Data as JSON: /api/errors/ce24b46e1409c4f0. Report an issue: GitHub.