Description
apify-client-python is adding streaming request bodies in apify/apify-client-python#1060. After it lands, set_record accepts a file-like object, an iterator of bytes/str chunks, or a streamed HttpResponse, and sends it chunked without buffering. The download direction (stream_record) has been there for a long time.
Crawlee has no equivalent at any layer: not in the KeyValueStoreClient / DatasetClient contracts, not in the KeyValueStore / Dataset frontends, and not in the backends. Passing a file object through today's API also corrupts the record silently on three of the five KVS backends.
#1931 asks for streaming KVS records and is still in solutioning. This issue is the wider investigation it needs: what the interfaces should look like, what each backend can actually do, and what the storages should expose.
Related
- feat: Stream request bodies from files, iterables, and responses apify-client-python#1060
- Streaming from/into key-value store records #1931
- Better KVS streaming support crawlee#2929
✍️ Drafted by Claude Code
Activity
vdusek commentedon Sep 16, 2026
What happens today
Uploads
KeyValueStore.set_value(src/crawlee/storages/_key_value_store.py:173) forwardsvalue: AnytoKeyValueStoreClient.set_value(src/crawlee/storage_clients/_base/_key_value_store_client.py:53). Neither says whatvaluemay be, and the backends disagree:_file_system/_key_value_store_client.py:302), SQL (_sql/_key_value_store_client.py:149) and Redis (_redis/_key_value_store_client.py:125) serialize through a chain that ends instr(value).encode('utf-8'). A file handle or a generator is stored as itsrepr, soawait kvs.set_value('out', open('big.bin', 'rb'))writes the literal text<_io.BufferedReader name='big.bin'>and reports success._memory/_key_value_store_client.py:112) keeps the object as it is and recordssys.getsizeof(value)as the size. A generator is then consumed by whichever reader gets to the record first.src/apify/storage_clients/_apify/_key_value_store_client.py:123) hands the value straight toset_record, so once feat: Stream request bodies from files, iterables, and responses apify-client-python#1060 lands this one backend streams correctly by accident, with nothing in Crawlee's types to promise it.So the same call is a working streaming upload on the platform and silent data loss on the local default.
Downloads
Every backend materializes the whole record. The file system client does
record_path.read_bytes(), SQL selects the full BLOB, Redis doeshget, and the Apify client callsget_recordeven thoughstream_recordis available.KeyValueStore.get_valuereturns a decoded value and there is no way to ask for a stream, so reading a 2 GB screenshot or a large NDJSON export costs 2 GB of RSS.Datasets
Dataset.export_to(src/crawlee/storages/_dataset.py:362) collects every item into a list (src/crawlee/_utils/file.py:193), serializes it into aStringIO, then passes the resultingstrtoset_value. A large dataset is held in memory at least twice before the first byte is written. This is the most obvious consumer of a streamingset_value, since the export is already produced incrementally.On the read side,
Dataset.iterate_itemsalready pages, so the gap there is smaller. Worth checking whetherget_datawith the defaultlimit=999_999_999_999deserves the same treatment.Questions to settle
KeyValueStoreClient.set_valuewiden itsvaluetype to include file-likes and (async) iterators, or does streaming get its own method (stream_value/set_value_stream) that backends opt into? The first keeps one call site, the second keepsAnyfrom meaning "anything, results may vary".bytesis correct and at least well defined, unlike today'srepr. The file system backend can stream a chunked write for real.AsyncIterator[bytes], an async context manager wrapping the response, or a file-like object. It has to work for a local file, a Redis field, and an HTTP response, and it has to make the Apify backend'sstream_recordusable through the storage.infer_mime_typecannot inspect a stream, so the content type has to default toapplication/octet-streamor be required from the caller, andsizeis unknown until the source is drained.Dataset.export_tostream into the KVS onceset_valuesupports it? That removes theStringIOand the fully materialized item list.KeyValueStore.setValuethere already accepts a stream, while reading one means dropping down to the resource client (Better KVS streaming support crawlee#2929).✍️ Drafted by Claude Code