chroma-core/chroma · error · ValueError

conditional transaction is closed

Error message

conditional transaction is closed

What it means

This ValueError comes from ConditionalHttpTransaction._ensure_open in chromadb/api/conditional_http.py: the conditional-transaction object has already been closed (its close() was called, typically after commit) and any further operation — prepare_get, record_get, buffer_add/update/delete/upsert, prepare_commit — raises 'conditional transaction is closed'. It is the Chroma equivalent of using a DB transaction after commit/rollback: the transaction is single-use.

Source

Thrown at chromadb/api/conditional_http.py:219

            "operations": self._operations.copy(),
        }

    def prepare_commit_payload(
        self,
    ) -> Optional[Tuple[ConditionalHttpScope, ConditionalHttpJsonPayload]]:
        prepared_commit = self.prepare_commit()
        if prepared_commit is None:
            return None
        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")

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Create a NEW ConditionalHttpTransaction (via the client's transaction entry point) for each unit of work instead of reusing the closed one
  2. Track committed/closed state in your wrapper and refuse or reopen (new transaction) on subsequent writes
  3. Ensure retry logic acquires a fresh transaction after a commit failure or success, rather than replaying onto the same object

Example fix

# before
tx = client.create_conditional_transaction()
tx_add(tx, ids=[...])
client.commit(tx)  # closes tx
tx_add(tx, ids=['more'])  # ValueError: conditional transaction is closed

# after
tx = client.create_conditional_transaction()
tx_add(tx, ids=[...])
client.commit(tx)  # tx closed here

tx = client.create_conditional_transaction()  # fresh transaction per unit of work
tx_add(tx, ids=['more'])
client.commit(tx)
Defensive patterns

Strategy: validation

Validate before calling

class TxnHandle:
    def __init__(self, client):
        self.client = client
        self._tx = None

    def begin(self):
        self._tx = self.client.create_conditional_transaction()  # or equivalent entry point
        return self._tx

    @property
    def tx(self):
        if self._tx is None or self._tx._closed:
            self.begin()  # fresh transaction; never reuse a closed one
        return self._tx

Type guard

def txn_is_open(tx) -> bool:
    return getattr(tx, '_closed', False) is False

Try / catch

try:
    buffered_op(tx, ...)
except ValueError as e:
    if 'conditional transaction is closed' in str(e):
        tx = client.create_conditional_transaction()  # start a new unit of work
        buffered_op(tx, ...)
    else:
        raise

Prevention

When it happens

Trigger: Using the transaction API (client-style conditional transactions over HTTP) and calling any buffered operation after the transaction has been committed and closed, or after an explicit close(first_inserted_record_offset=...). Typical code: reuse of a stored transaction object across requests, or a loop that commits inside the body and keeps writing afterwards.

Common situations: Long-lived application objects holding a transaction handle across requests; retry wrappers that replay operations onto an already-committed transaction; frameworks where a per-client singleton transaction is accidentally shared; misunderstanding that close() finalizes the transaction permanently.

Related errors


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