Solution skill
Dash Callbacks Not Firing or Firing in a Loop: Dependency Rules, Circular Callbacks, prevent_initial_call
Complete solution
Shows how to fix dash Callbacks Not Firing or Firing in a Loop: Dependency Rules, Circular Callbacks, prevent_initial_call. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.
TL;DR
For "from dash.exceptions import PreventUpdate": 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.triggeredid` returns the component id (or the id dict for pattern matching) that
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
...When to use
You are seeing this: State values are passed to the function but never fire it. Use this skill when you run into "Dash Callbacks Not Firing or Firing in a Loop: Dependency Rules, Circular Callbacks, preventinitialcall".
When not to use
If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.
Versions
No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.
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.
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 Inputs first, then all States. 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:
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:
app = Dash(prevent_initial_callbacks=True) # every callback skips the initial fireIndividual 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 Inputs, 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:
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:
@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) + 1Fix 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:
@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_dateReturning 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:
@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-wideprevent_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:
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]:
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
Callback never fires: is the changing property declared as
Input, notState? Does the component id match the layout exactly?- Callback fires on load but shouldn't: add
prevent_initial_call=True. Callback fires too often: check how many
Inputs it has and branch onctx.triggered_id.CircularDependencyat startup: find the cycle; merge into one callback with self-referenced Input/Output, or break it with an intermediate store.Duplicate output error:
allow_duplicate=Trueplusprevent_initial_call=True.Noticeable lag on hover/slider/typing: move the cheap transform to a clientside callback; keep DB and heavy compute serverside with caching.
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.
Variants
Other phrasings of the same problem that show up in reports:
from dash import Dash, Input, Output, State, callback
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}!"Evidence
Dash docs on clientside callbacks state serverside overhead comes from large payloads, high-frequency calls (network latency/queuing/handshake), and multi-roundtrip chains, and that clientsidecallback uses the same Input/Output declaration with a JS function; docs also note clientside callbacks cannot access server globals or DB. Dash docs on duplicate-callback-outputs (Dash 2.9+): allowduplicate=True requires preventinitialcall=True or preventinitialcallbacks="initialduplicate", and simultaneous update order is not guaranteed. GitHub PR plotly/dash#1228 added preventinitialcall per callback and app-wide preventinitialcallbacks (per-callback False override). Forum analysis of circular dependencies documents the Dash rule: cross-callback cycles are rejected (CircularDependency), while the same component property may be both Input and Output of one callback; fixes are merging synced props into one callback with ctx.triggeredid branching, or unidirectional flows through dcc.Store with PreventUpdate guards.
By @issueatlas · Version 2 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_upcnQjGhB2ClzDHvfT6qJA" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Dash Callbacks Not Firing or Firing in a Loop: Dependency Rules, Circular Callbacks, prevent_initial_call <span>— Vectle</span></a>