# Fix dcc.Store serialization, storage-type, and size problems in Dash
Use this when `dcc.Store` misbehaves: a callback errors with "not JSON
serializable", stored data vanishes after a page refresh, data from one user
leaks into another session, the app gets sluggish because megabytes shuttle
back and forth on every callback, or a callback can't read the store's initial
value on page load. Classify from the symptom FIRST — the fixes contradict
each other (e.g. "persist it" vs "don't store it at all").
`dcc.Store` keeps JSON data in the *browser*, not on the server. Its props:
`storage_type` (`'memory'` default, `'local'`, `'session'`), `data`,
`modified_timestamp` (read-only), `clear_data` (boolean; set `True` to wipe).
## 1. Classify the failure mode
- **A. Serialization error.** The callback fails with
`TypeError: Object of type DataFrame is not JSON serializable`, or Dash's
wrapper: `dash.exceptions.InvalidCallbackReturnValue: The callback for an `Output(...)` target returned a value having type ... which is not JSON
serializable.` The value assigned to `data` must be JSON-compatible.
- **B. Data disappears on refresh — or persists when it shouldn't.**
`storage_type="memory"` (the default) is cleared on every page refresh.
`'local'` (`window.localStorage`) survives browser restarts; `'session'`
(`window.sessionStorage`) survives refresh but dies with the tab.
- **C. The app is slow and network payloads are huge.** The store holds a
large dataset and the docs' warning applies: that data "will be transported
over the network between each callback." Check the `_dash-update-component`
response size in dev tools.
- **D. Can't read the store's initial value on page load.** If `data` is only
ever written by a callback output, a callback taking `Input("store", "data")`
doesn't fire for the initial value.
## 2. Fix A: serialize before storing
Convert non-JSON types at the boundary. The Dash docs' canonical pattern for
DataFrames:
```python
from dash import Input, Output, callback
import pandas as pd
@callback(Output("store", "data"), Input("dropdown", "value"))
def clean_data(value):
cleaned_df = slow_processing_step(value)
# JSON string in the store; never the raw DataFrame
return cleaned_df.to_json(date_format="iso", orient="split")
@callback(Output("graph", "figure"), Input("store", "data"))
def update_graph(jsonified):
dff = pd.read_json(jsonified, orient="split")
return create_figure(dff)
```
General conversions: `datetime` → `.isoformat()`; numpy array → `.tolist()`;
DataFrame → `.to_json()` or `.to_dict("records")`; custom objects → dicts.
The check happens on the *returned* value, so the error names the exact type
to convert.
## 3. Fix B: choose the storage type deliberately
```python
from dash import dcc
dcc.Store(id="session-cache", storage_type="memory") # default: per page-load
dcc.Store(id="user-prefs", storage_type="local") # survives browser quit
dcc.Store(id="tab-state", storage_type="session") # survives refresh, dies with tab
```
- Data gone after refresh → you wanted `'local'` or `'session'`, not the
`'memory'` default.
- One user's data appearing for another user on a shared machine, or stale
data months later → that's `'local'` doing exactly what it does: it
persists indefinitely and is shared per origin. Switch to `'session'` or
`'memory'`, and use `clear_data=True` (or write `None`) on logout/reset.
- Same store `id` reused across apps on one origin (e.g. several dev apps on
the loopback interface, port 8050) → `'local'` data collides across apps. Namespace the id.
Size limits are browser quotas, not Dash's: the docs say it's generally safe
to store up to 2MB in most environments, and 5–10MB in desktop-only apps —
and UTF-16 encoding can halve effective capacity. Treat the store as small
by design.
## 4. Fix C: don't ship the dataset — ship a pointer or an aggregation
If the store holds more than a few hundred KB, restructure. The docs'
guidance: compute aggregations upfront and transport those, because your app
"likely won't be displaying 10MB of data, it will just be displaying a subset
or an aggregation of it."
- **Aggregate before storing:** the producing callback does the groupby /
filter / downsample and stores the small result.
- **Store an id, keep data server-side:** store a dataset key or session id in
the store, and keep the real data in a server-side cache (Flask-Cache,
Redis, or disk). This also works across gunicorn workers, where in-memory
globals do not.
- **Use the store as a signal:** store a tiny payload (or just bump
`modified_timestamp`) and have downstream callbacks re-read from the
server-side cache when the signal changes — the docs' "caching and
signaling" pattern. The expensive computation then runs once per unique
input instead of once per callback per worker.
The old "hidden div" trick for smuggling data is explicitly not recommended
anymore; `dcc.Store` in browser memory replaces it.
## 5. Fix D: read initial data via `modified_timestamp`
A callback can't observe the store's initial `data` through `Input(...,
"data")` when `data` is only ever produced by another callback's output.
Use the read-only `modified_timestamp` as the trigger and `data` as `State`:
```python
@callback(
Output("output", "children"),
Input("store", "modified_timestamp"),
State("store", "data"),
)
def on_load(ts, data):
if ts == -1 or data is None:
raise PreventUpdate # nothing stored yet
return render(data)
```
`modified_timestamp` fires on every write (even when the value is unchanged),
whereas `Input("store", "data")` fires only when the value changes — pick the
trigger that matches the semantics you want.
## 6. Checklist for a misbehaving store
1. Error mentions serializability → convert at the boundary (step 2); check
the exact type named in the error.
2. Wrong lifetime → set `storage_type` explicitly; default `'memory'` clears
on refresh; `'local'` persists indefinitely per origin.
3. Payloads > ~1MB in `_dash-update-component` → aggregate upfront, store ids,
or move data server-side with the store as a signal.
4. Need the initial value on load → `Input("store", "modified_timestamp")` +
`State("store", "data")`, guarding `ts == -1`.
5. No callbacks mutating globals as an alternative — with multiple gunicorn
workers, globals aren't shared and sessions leak across users.