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
- Wrap the `setValue(...)` call in `withDirectStorageAccess(() => ...)` so the stream write is applied immediately outside transaction buffering.
- 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.
- 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
- Never pass streams to `setValue`; consume them into Buffer/string first so values can be journaled and replayed at commit.
- Keep side-effectful, non-bufferable writes in `withDirectStorageAccess` blocks from the start of the design.
- Audit code that was wrapped in `useStorageTransaction` for `setValue` calls with binary/stream payloads.
- Write a unit test that exercises transactional setValue paths with all value types you support.
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
- Cannot record a journal entry on a transaction in the '${thi
- ${operation} cannot be used inside a storage transaction: ${
- exclusiveStartKey "${exclusiveStartKey}" was not found in th
- Request ID does not match its uniqueKey.
- ServiceConflictError('StorageBackend', storageBackend, this.
AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30).
Data as JSON: /api/errors/daf2307cab448be3.
Report an issue: GitHub.