# Fix dcc.Store serialization, storage-type, and size problems in Dash

Classify a dcc.Store problem from its symptom (not-JSON-serializable callback errors, data lost on refresh vs persisting too long, multi-MB network payloads on every callback, or unreadable initial values on page load) and apply the matching fix: serialize at the boundary (to_json/to_dict/tolist/isoformat), pick memory/local/session storage deliberately, aggregate upfront or keep data server-side with the store as a signal, or trigger on modified_timestamp with data as State.

Exact reference: {"kind":"skill_version","skill_id":"skl__MWJ4AKJEVklJLKM-EH-3A","version_id":"skv_FLo8tyG899PzS73pBQzHUA"}

Applicability: [{"constraint":">=2.0","technology":"Dash","version_scheme":"semver"}]

# 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.


## Supporting basis and limitations

Built from the Dash documentation (dcc.Store reference: storage_type memory/local/session semantics, modified_timestamp read-only, clear_data, 2MB safe / 5-10MB desktop-only storage limits, initial-data via modified_timestamp+State; Sharing Data Between Callbacks: serialize with to_json orient=split, network cost of large stores, caching-and-signaling pattern) and recurring Plotly community forum threads on InvalidCallbackReturnValue serialization errors.

## Change and rationale

New skill: fix dcc.Store serialization, storage-type, and size problems in Dash.

dcc.Store is the recommended replacement for the deprecated hidden-div pattern and sits at the center of multi-callback Dash apps, yet forum questions show agents repeatedly hitting the same four traps: non-JSON types, the memory-default refresh wipe, oversized stores slowing every callback, and the initial-data-on-load gap. This skill adds a classify-first decision procedure so the agent picks the fix that matches the symptom instead of applying them blindly.
