# Dash Callback Firing Rules: Why Callbacks Don't Fire, Fire Too Often, or Loop
A field guide to the mechanics of `@callback` / `@app.callback` dependency wiring:
when a callback fires, why it silently doesn't, how to stop the initial fire,
how to break circular dependencies, and when a clientside callback beats a
serverside roundtrip.
## The one rule that explains most problems
A callback fires when **any `Input` value changes**, plus once when the page or
layout first loads. `State` values are passed to the function but **never fire
it**. That asymmetry is the source of most "callback not firing" reports.
```python
from dash import Dash, Input, Output, State, callback
app = Dash()
@callback(
Output("result", "children"),
Input("submit-btn", "n_clicks"), # fires when this changes
State("name", "value"), # read, but never fires
prevent_initial_call=True,
)
def submit(n_clicks, name):
return f"Hello, {name}!"
```
If you declared the button as `State` and the text field as `Input`, the
callback would fire on every keystroke and ignore the button. When a callback
"does nothing", check the wiring first: the property that changed must be an
`Input`.
Function argument order follows declaration order: all `Input`s first, then
all `State`s. The decorator groups them positionally, so keep the order
aligned with the function signature.
## Gotcha 1: Every callback fires once on page load
On load, every callback fires with initial values (e.g. `n_clicks` is `None`,
dropdown values are their defaults). This is why you see code like:
```python
from dash.exceptions import PreventUpdate
@callback(
Output("result", "children"),
Input("submit-btn", "n_clicks"),
)
def submit(n_clicks):
if n_clicks is None:
raise PreventUpdate
...
```
`prevent_initial_call=True` removes the need for that guard entirely: the
callback only runs on a genuine `Input` change. The same keyword works on
`clientside_callback`.
App-wide control is available at construction:
```python
app = Dash(prevent_initial_callbacks=True) # every callback skips the initial fire
```
Individual callbacks can still opt back in with
`prevent_initial_call=False`. There is also the special app-wide value
`"initial_duplicate"`, which prevents initial calls only for callbacks whose
outputs are shared with other callbacks (see the duplicate-outputs section).
Practical note: in apps with dynamically added components, the "initial call"
is not only on page load — a callback also fires when a *new* matching input
is inserted into the layout (for example pattern-matching callbacks firing for
every index when a new indexed button is added). `prevent_initial_call=True`
is the fix for that variant too.
## Gotcha 2: Any Input change fires the whole callback
If a callback has four `Input`s, changing any one of them runs the full
function. This is by design, but it surprises people who assume a callback
only responds to "its" input. Use `ctx.triggered_id` to branch on which input
actually changed:
```python
from dash import ctx
@callback(
Output("graph", "figure"),
Output("table", "data"),
Input("refresh-btn", "n_clicks"),
Input("filter", "value"),
prevent_initial_call=True,
)
def update(n_clicks, filter_value):
triggered = ctx.triggered_id
if triggered == "refresh-btn":
return refresh_figure(), refresh_table()
return filtered_figure(filter_value), filtered_table(filter_value)
```
`ctx.triggered_id` returns the component id (or the id dict for pattern
matching) that caused the current firing. Without it, multi-input callbacks
recompute everything on every change.
## Gotcha 3: Chains cascade — outputs become inputs
When a callback writes to a component property, every callback that lists that
property as an `Input` fires next. This is how multi-step chains work, and it
is also how accidental feedback loops form.
The most common loop shape: callback A updates a property that callback B
reads as an `Input`, and callback B writes a property that callback A reads.
Dash detects dependency cycles at startup and raises a
`CircularDependency` error (you will see `Cycle detected` in the traceback
when the app fails to register the callbacks). Cross-callback cycles are
**not** allowed.
Two legal exceptions, and two fixes:
**Allowed: self-reference in one callback.** A callback may list the *same*
component property as both an `Input` and an `Output`:
```python
@callback(
Output("counter", "data"),
Input("counter", "data"),
Input("add-btn", "n_clicks"),
prevent_initial_call=True,
)
def increment(current, n_clicks):
return (current or 0) + 1
```
**Fix 1: merge the cycle into one callback.** If two properties must stay in
sync (for example a URL search string and a date picker), use a single
callback that takes all synced properties as both `Input` and `Output`, then
branch on `ctx.triggered_id` so only the *other* properties get updated:
```python
@callback(
Output("url", "search"),
Output("picker", "start_date"),
Input("url", "search"),
Input("picker", "start_date"),
prevent_initial_call=True,
)
def sync(url_search, start_date):
triggered = ctx.triggered_id
if triggered == "url":
return url_search, parse_date_from_search(url_search)
return build_search_from_date(start_date), start_date
```
Returning the input's own value unchanged avoids re-triggering the syncing
property.
**Fix 2: split unidirectional flows with an intermediate store.** Keep
A-to-B and B-to-A as separate callbacks through a `dcc.Store`, and make each
one no-op unless the store says its side changed (raise `PreventUpdate` when
it is not your turn). This is more code than Fix 1 but scales better when
many properties need syncing.
## Gotcha 4: The same Output used by two callbacks
By default, one component property may be an `Output` of at most one
callback; a second registration raises a duplicate-output error. Since Dash
2.9 you can opt into sharing:
```python
@callback(
Output("graph", "figure", allow_duplicate=True),
Input("draw-btn", "n_clicks"),
prevent_initial_call=True,
)
def draw(n_clicks):
return build_figure()
@callback(
Output("graph", "figure"),
Input("reset-btn", "n_clicks"),
prevent_initial_call=True,
)
def reset(n_clicks):
return empty_figure()
```
Rules for `allow_duplicate=True`:
- Every callback sharing the output must set `prevent_initial_call=True`
(or set the app-wide `prevent_initial_callbacks="initial_duplicate"`), so
the callbacks don't all fire at once on page load.
- When two such callbacks fire simultaneously, update order is **not
guaranteed** — don't have them both rewrite overlapping parts of the same
property.
If the two callbacks instead share an `Input`, that is fine — multiple
callbacks may listen to the same input.
## Serverside vs clientside: the latency tradeoff
A normal Python callback costs a full network roundtrip on every fire: the
browser serializes the `Input`/`State` values, the server runs your function,
and the response travels back. That overhead dominates when the callback:
- moves large payloads (big figures, long tables),
- fires very often (hovering, sliders, rapid keystrokes — a browser allows
only a handful of concurrent requests, so bursts queue up), or
- sits in a chain where each hop needs another roundtrip.
`clientside_callback` runs the same `Input`/`Output` declaration in the
browser as JavaScript — no request at all:
```python
from dash import clientside_callback, ClientsideFunction, Input, Output
clientside_callback(
ClientsideFunction(namespace="clientside", function_name="double_value"),
Output("doubled", "children"),
Input("number", "value"),
prevent_initial_call=True,
)
```
with the function defined in `assets/[ANY_JS_FILE]`:
```javascript
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
double_value: function(value) {
return value * 2;
}
}
});
```
Or pass the JS function as an inline string directly to
`clientside_callback`. Both forms accept `prevent_initial_call=True`.
Decision rule:
- **Clientside** when the transform is cheap, purely presentational (format a
string, toggle visibility, arithmetic), and needs to respond instantly to
high-frequency inputs.
- **Serverside** when the callback needs Python libraries, server-side
globals, secrets, or a database call. A clientside function cannot reach
anything on the server; if your data lives behind a DB query, the roundtrip
is unavoidable (cache the query result instead, e.g. with Flask-Caching or
an in-memory store keyed by the inputs).
## Debugging checklist
1. Callback never fires: is the changing property declared as `Input`, not
`State`? Does the component id match the layout exactly?
2. Callback fires on load but shouldn't: add `prevent_initial_call=True`.
3. Callback fires too often: check how many `Input`s it has and branch on
`ctx.triggered_id`.
4. `CircularDependency` at startup: find the cycle; merge into one callback
with self-referenced Input/Output, or break it with an intermediate store.
5. Duplicate output error: `allow_duplicate=True` plus
`prevent_initial_call=True`.
6. Noticeable lag on hover/slider/typing: move the cheap transform to a
clientside callback; keep DB and heavy compute serverside with caching.