Hi everyone,
I am building an app on a virtual machine I have structured my architecture to run as two completely separated processes in two different terminals:
-
Terminal 1 (Background Runner): Periodically processes raw data using heavy LLM translation/extraction tasks, outputting a cleaned
alerts.jsonlfile. -
Terminal 2 (Streamlit UI): A presentation layer dashboard running 24/7 that reads that generated file and displays charts/maps.
The Goal:
Since Streamlit is running continuously, I want it to load the pre-processed data instantly from memory for my users. However, the exact millisecond my background runner completes a new run and updates the alerts.jsonl file, I need Streamlit to instantly detect this change and sync without requiring users to wait out a long static TTL (like 24h).
My Proposed Strategy (The Outer/Inner Function Pattern):
I want to use a non-cached outer function to check the file state, which then passes the modification timestamp as a “cache invalidation key” to a cached inner function:
Python
import os
import pandas as pd
import streamlit as st
# 1. Outer function (Runs on every user interaction/refresh)
def load_and_process_data():
alerts_path = "results/alerts.jsonl"
# Get the file's last modified timestamp as our cache key
mtime = os.path.getmtime(alerts_path) if os.path.exists(alerts_path) else 0.0
# Pass the timestamp to the cached worker function
return _cached_load_and_process(mtime)
# 2. Inner function (Cached)
# We set max_entries=1 so that the moment a new timestamp forces a reload,
# the old dataset is immediately evicted from the server's RAM.
@st.cache_data(ttl=86400, max_entries=1)
def _cached_load_and_process(timestamp):
alerts_path = "results/alerts.jsonl"
centroids_path = "src/dashboard/country_centroids.csv"
with open(alerts_path, "r") as f:
df_alerts = pd.read_json(f, lines=True)
with open(centroids_path, "r") as f:
df_centroids = pd.read_csv(f)
df_alerts["pub_date"] = pd.to_datetime(df_alerts["pub_date"]).dt.date
df_alerts_mapped = pd.merge(
df_alerts,
df_centroids,
left_on="iso2_code",
right_on="iso_alpha2",
how="inner"
)
return df_alerts_mapped
-
Do I actually need
ttl=86400if I am already usingmax_entries=1? Since the cache size is capped at 1, does setting a TTL add any benefit (like clearing memory when the app is idle), or is it redundant because the timestamp parameter handles invalidation automatically anyway? -
Is this “Outer/Inner” pattern still the standard/recommended approach for syncing Streamlit with external background tasks modifying local files?
-
Is using
max_entries=1the safest way here to prevent RAM bloating when the background worker generates new files over time? Or is there a cleaner built-in method I’m missing? -
Eventually, I will migrate my database to MongoDB. Does this pattern transition smoothly if I replace
os.path.getmtime()with a function that fetches the newest document’s_idfrom Mongo?
Would love to hear how other developers handle instant cache invalidation on file/DB updates. Thank you!