apify/crawlee · error

${operation} cannot be used inside a storage transaction: ${

Error message

${operation} cannot be used inside a storage transaction: ${reason} If you really need it, wrap the call in withDirectStorageAccess(() => ...) - operations performed there are applied immediately and are not rolled back.

What it means

This error is thrown by `KeyValueStore.setValue()` when it is called inside an active storage transaction (`transaction !== undefined`) with a value that cannot participate in the transaction. For stream values the store throws unconditionally via `operationRejectedInTransaction` because a stream can only be consumed once and cannot be buffered until commit (it could not serve both read-your-own-writes and the commit replay). The error tells you to wrap the call in `withDirectStorageAccess()` so the write is applied immediately and is not subject to rollback.

Source

Thrown at packages/core/src/storages/key_value_store.ts:538

        parseArgument(key, setValueKeySchema);
        if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
            throw new Error(
                'The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.',
            );
        }
        // The parse result is a fresh copy, so we never update what user passed.
        const optionsCopy = parseArgument(options, recordOptionsSchema);

        // The whole transaction branch sits *above* the auto-saved cache update below, so a buffered
        // write touches nothing outside the journal. That cache is shared, process-lifetime frontend
        // state, so mutating it here would survive a rollback and later be persisted by `persistState`.
        // The commit replay re-enters this method with no active transaction and updates it then.
        if (transaction) {
            if (isStream(value)) {
                // A stream cannot serve both a read-your-own-writes read and the commit replay. The
                // transaction is known-active here, so throw directly rather than via the conditional guard.
                throw operationRejectedInTransaction(
                    `KeyValueStore.setValue() with a stream value (key "${key}")`,
                    'a stream can only be consumed once, so it cannot be buffered until commit.',
                );
            }

            // Validation only, result discarded: the journal snapshot (`structuredClone`) accepts values
            // JSON cannot, which would otherwise only throw at a later read or at commit.
            if (value !== null) {
                serializeValue(value, optionsCopy.contentType);
            }

            // One snapshot serves both the reads and the commit replay; `null` is a tombstone.
            transaction.recordJournalEntry({
                type: 'keyValueStore',
                participant: this,
                storageId: this.id,
                key,
                value: value === null ? null : snapshotValue(value),

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Wrap the `setValue(...)` call in `withDirectStorageAccess(() => ...)` so the stream write is applied immediately outside transaction buffering.
  2. Convert the stream to a Buffer/string (e.g. via `streamToBuffer` or consuming it fully) before `setValue`, since non-stream values can be journaled and replayed at commit.
  3. Move the stream write out of the transactional section entirely, performing it before opening or after committing the transaction.

Example fix

// before
await useStorageTransaction(async (tx) => {
    await tx.setValue('report.pdf', pdfStream); // throws
});

// after
await withDirectStorageAccess(async () => {
    await store.setValue('report.pdf', pdfStream);
});
Defensive patterns

Strategy: validation

Validate before calling

// Before calling setValue inside a transaction, reject stream values
import { isStream } from '@crawlee/core/utils';
if (isStream(value)) {
  throw new TypeError(`Value for key "${key}" is a stream; buffer it or use withDirectStorageAccess first.`);
}
await store.setValue(key, value);

Type guard

function isBufferableValue(v: unknown): v is string | Buffer | Record<string, unknown> {
  return typeof v === 'string' || Buffer.isBuffer(v) || (typeof v === 'object' && v !== null && !('pipe' in v) && !(v instanceof ReadableStream));
}

Try / catch

try {
  await store.setValue(key, value);
} catch (err) {
  if (String(err.message).includes('cannot be used inside a storage transaction')) {
    // Stream value inside a transaction: apply directly, outside the journal
    await withDirectStorageAccess(() => store.setValue(key, value));
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling `keyValueStore.setValue(key, someStream)` (or any setValue whose value is detected as a stream via `isStream(value)`) inside a `useStorageTransaction()`/transactional block. Callers like `ensurePersistStateEvent` and `commitJournalEntries` route through setValue, so any transactional code path that passes a stream hits this.

Common situations: Storing a `ReadableStream`, Node `Readable`, or file stream (e.g. a downloaded file or generated binary blob) into the key-value store while inside a run-scoped storage transaction introduced for atomic state persistence; code that worked before transactions were added starts failing after being wrapped in a transaction.

Related errors


AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30). Data as JSON: /api/errors/daf2307cab448be3. Report an issue: GitHub.