Fix: Dash Not Working — Callback Errors, Pattern Matching, and State Management
Part of: Python Errors
Quick Answer
How to fix Dash errors — circular dependency in callbacks, pattern matching callback not firing, missing attribute clientside_callback, DataTable filtering not working, clientside JavaScript errors, Input Output State confusion, and async callback delays.
The Error
Your callback doesn’t fire when you expect it to — or it fires in a loop and locks the UI:
Circular dependency found in callback graph when connecting:
Input: page-dropdown.value -> Output: graph.figureOr you try to use pattern-matching callbacks and nothing happens:
@callback(
Output({'type': 'graph', 'index': ALL}, 'figure'),
Input({'type': 'button', 'index': ALL}, 'n_clicks'),
)
# Callback never triggers despite button clicksOr the app crashes on load with:
AttributeError: module 'dash' has no attribute 'dependencies'Or a Dash DataTable filter doesn’t update the data, or a clientside callback throws JavaScript errors that block the entire UI.
Dash is Plotly’s reactive web framework — it drives browser updates from Python callbacks triggered by user interactions. When callbacks reference each other cyclically, when you misunderstand the pattern-matching syntax, or when you mix old and new API patterns, you get silent failures or hard crashes that are difficult to trace. This guide covers the root causes and fixes.
Why This Happens
Dash callbacks form a directed acyclic graph (DAG) — each callback has inputs that trigger it and outputs it produces. If callback A reads the output of callback B, which reads the output of callback A, the DAG has a cycle and can’t be built. The pattern-matching callback syntax (MATCH, ALL, ALLSMALLER) is powerful but requires exact ID structure alignment — a small mismatch in the ID dictionary keys silently breaks the pattern.
The Dash 2.0 API deprecated dash.dependencies in favor of importing Input, Output, State directly from dash. Code written for Dash 1.x breaks on import in 2.x without the right changes.
Platform and Environment Differences
Dash callbacks behave the same way on every operating system, but how you serve the app and where you deploy it changes which failures you hit.
Flask dev server vs Gunicorn. app.run_server(debug=True) uses Flask’s development WSGI server — single process, hot reload, verbose tracebacks. Production should use Gunicorn or uWSGI in front of app.server (the underlying Flask instance). With Gunicorn, the worker count matters. Each worker is a separate Python process with its own memory and its own copy of any global state. If you store data in module-level variables instead of dcc.Store or a database, two browsers hitting two different workers see different data:
gunicorn -w 4 -b 0.0.0.0:8050 --timeout 120 app:serverLong callbacks under multi-worker setups require a shared cache backend (Diskcache or Celery), not the in-memory default — workers don’t share Python dicts.
Render, Fly.io, and Cloud Run. Render and Fly auto-detect Python apps and expect a Procfile like web: gunicorn app:server --bind 0.0.0.0:$PORT. Cloud Run requires the app to bind to 0.0.0.0 and to the port from $PORT (default 8080), not 127.0.0.1:8050. A common failure: locally you run python app.py and it binds to 127.0.0.1:8050; deployed, the platform never reaches your app and the health check fails. For Render and Fly deployment workflows that affect any Python web app, see Fly deploy not working.
AWS Lambda via Mangum or serverless-wsgi. Dash can run on Lambda by wrapping app.server (Flask) with Mangum. Cold starts hurt — every callback reload pays the import cost. Long callbacks won’t work because Lambda has a 15-minute hard timeout and no persistent worker. Use App Runner, ECS, or EC2 if your callbacks run more than a few seconds.
Docker WSGI worker count. The Gunicorn rule of thumb (workers = 2 * CPU + 1) is wrong on small containers — each worker holds a full Python interpreter plus pandas/numpy, easily 200–500 MB. On a 1 GB container, 4 workers OOM-kill. Start with 2 workers and raise only if CPU is saturated.
Behind nginx reverse proxy. WebSocket support requires explicit upgrade headers — without them, Dash’s hot reload and any callback that uses long polling silently drops. Set proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";. For the standard nginx proxy block for Python apps, see nginx websocket proxy not working.
Dash Enterprise. Plotly’s commercial host adds authentication, snapshots, and a managed deploy pipeline. Locally working callbacks can fail on Enterprise because the host strips certain HTTP headers and enforces CSP rules that block inline JavaScript. If you use clientside callbacks from CDN scripts, list those CDNs in the app’s allowed external scripts.
WSL2 caveat. On Windows + WSL2, running app.run_server() inside WSL exposes the app to localhost:8050 from Windows automatically, but only over IPv4. If your browser uses IPv6 (::1) you get connection refused. Bind explicitly with app.run_server(host='0.0.0.0').
Fix 1: Circular Dependency in Callbacks
CircularDependencyError: Circular dependency found in callback graph
when connecting: Input: x.value -> Output: y.figureDash can’t execute a DAG where callback A depends on callback B’s output, and callback B depends on callback A’s output. This often happens unintentionally when you have:
- Callback 1: Input
dropdown.value→ Outputgraph.figure - Callback 2: Input
graph.figure→ Outputdropdown.value
The graph can’t be topologically sorted — both callbacks need the other to complete first.
from dash import dcc, html, callback, Input, Output
import plotly.graph_objects as go
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(id='metric', options=['sales', 'profit'], value='sales'),
dcc.Graph(id='chart'),
])
# WRONG — Circular dependency
@callback(
Output('chart', 'figure'),
Input('metric', 'value'),
)
def update_chart(metric):
# This tries to read from chart to update dropdown
return go.Figure()
@callback(
Output('metric', 'value'),
Input('chart', 'figure'), # ← CIRCULAR: chart depends on metric, metric depends on chart
)
def update_metric(fig):
return 'sales'The fix — break the cycle by using State instead of Input for one direction:
from dash import dcc, html, callback, Input, Output, State, Button
app.layout = html.Div([
html.Button('Update', id='update-btn'),
dcc.Dropdown(id='metric', options=['sales', 'profit'], value='sales'),
dcc.Graph(id='chart'),
])
# CORRECT — No circular dependency
@callback(
Output('chart', 'figure'),
Input('update-btn', 'n_clicks'),
State('metric', 'value'), # Read metric, don't listen for changes
)
def update_chart(n_clicks, metric):
return go.Figure(data=[go.Bar(x=[1, 2, 3], y=[metric == 'sales', metric == 'profit', 1])])Input vs State:
Input | State |
|---|---|
| Triggers callback on every change | Does NOT trigger callback |
| Value passed as a callback argument | Value read inside callback but doesn’t cause execution |
| Creates a dependency edge in the DAG | No dependency edge |
Use Input for values that trigger re-computation, State for values you read but don’t depend on for execution. This breaks circular dependencies and improves performance (fewer redundant callback fires).
Detect cycles early:
from dash.graph import contains_circular_dependencies
if contains_circular_dependencies(app.layout):
print("Circular dependency detected!")Fix 2: Pattern-Matching Callbacks — MATCH, ALL, ALLSMALLER
# Callback never fires despite matching button clicks
@callback(
Output({'type': 'output', 'index': MATCH}, 'children'),
Input({'type': 'button', 'index': MATCH}, 'n_clicks'),
)
def update_output(n_clicks):
return f"Clicked {n_clicks} times"Pattern-matching callbacks allow dynamic component IDs. The ID must be a dictionary with consistent keys across Input and Output. A mismatch in the keys silently prevents the callback from firing.
Common mistake — ID key mismatch:
from dash import dcc, html, callback, Input, Output
from dash.dependencies import MATCH, ALL
app.layout = html.Div([
html.Button('Add Item', id='add-btn'),
html.Div(id='container', children=[]),
])
# Add items dynamically
@callback(
Output('container', 'children'),
Input('add-btn', 'n_clicks'),
)
def add_item(n_clicks):
return [
html.Div([
dcc.Input(id={'type': 'input', 'index': i}), # ID has 'type' and 'index'
html.Button('Update', id={'type': 'btn', 'index': i}),
])
for i in range(n_clicks or 0)
]
# WRONG — Output ID has different structure
@callback(
Output({'type': 'output', 'index': MATCH}, 'children'), # 'output' != 'input'
Input({'type': 'btn', 'index': MATCH}, 'n_clicks'),
)
def update_item(n_clicks):
return f"Updated: {n_clicks}" # Never fires because 'type' values don't match
# CORRECT — Output ID must correspond to an actual Input
@callback(
Output({'type': 'input', 'index': MATCH}, 'value'), # Matches the Input's 'type'
Input({'type': 'btn', 'index': MATCH}, 'n_clicks'),
)
def update_input(n_clicks):
return f"Updated: {n_clicks}"MATCH — triggers the callback when the IDs have matching field values. In the above, only the button with {'type': 'btn', 'index': 0} triggers the callback for the input with {'type': 'input', 'index': 0} (matching index).
ALL — collects all matching IDs into a list:
@callback(
Output('summary', 'children'),
Input({'type': 'input', 'index': ALL}, 'value'),
)
def summary(values):
# values is a list: [value_0, value_1, value_2, ...]
return f"Total: {sum(v for v in values if isinstance(v, (int, float)))}"ALLSMALLER — collects all IDs with an index smaller than the triggering ID:
@callback(
Output({'type': 'cumsum', 'index': MATCH}, 'children'),
Input({'type': 'input', 'index': MATCH}, 'value'),
Input({'type': 'input', 'index': ALLSMALLER}, 'value'),
)
def cumulative_sum(current_val, earlier_vals):
total = sum([v for v in (earlier_vals or []) if isinstance(v, (int, float))])
if isinstance(current_val, (int, float)):
total += current_val
return f"Sum: {total}"Pro Tip: Print the component structure during development to verify IDs match:
import json
from dash import html
print(json.dumps(app.layout, indent=2, default=str))
# Inspect the output to see actual ID structuresFix 3: Deprecated dash.dependencies Import
AttributeError: module 'dash' has no attribute 'dependencies'
ImportError: cannot import name 'Input' from 'dash.dependencies'Dash 2.0+ moved Input, Output, State, and pattern-matching dependencies directly into the dash module. Code written for Dash 1.x must be updated.
Old (Dash 1.x) — broken in 2.x:
from dash.dependencies import Input, Output, State, MATCH, ALLNew (Dash 2.x+):
from dash import Input, Output, State, ALL, MATCH, ALLSMALLER, callbackMigration checklist:
| Dash 1.x | Dash 2.x |
|---|---|
from dash.dependencies import Input, Output, State | from dash import Input, Output, State |
@app.callback(Output(...), Input(...)) | @callback(Output(...), Input(...)) |
dash.dependencies.MATCH | dash.MATCH (or from dash import MATCH) |
from dash import dependencies as dep | Not needed — use from dash import ... directly |
app.run_server(debug=True) | app.run_server(debug=True) (same) |
Check your Dash version:
pip show dash
# Version: 2.x.xIf you’re on Dash 2.x and see dash.dependencies errors, update all imports.
Fix 4: Clientside Callbacks — JavaScript and Errors
clientside callback for output ... raised an error:
ReferenceError: x is not definedClientside callbacks execute JavaScript in the browser instead of Python on the server — they’re fast and don’t require a server round-trip. But a typo in the JavaScript crashes silently or doesn’t update.
from dash import dcc, html, Input, Output, clientside_callback
app.layout = html.Div([
dcc.Input(id='input', value=''),
html.Div(id='output'),
])
# Define JavaScript function
app.clientside_callback(
'''
function(input_val) {
return input_val.toUpperCase();
}
''',
Output('output', 'children'),
Input('input', 'value'),
)Common mistake — undefined variables in JavaScript:
# WRONG — 'x' is not defined
app.clientside_callback(
'''
function(input_val) {
return x + input_val; // ReferenceError
}
''',
Output('output', 'children'),
Input('input', 'value'),
)
# CORRECT — use parameters
app.clientside_callback(
'''
function(input_val) {
return input_val + input_val;
}
''',
Output('output', 'children'),
Input('input', 'value'),
)Multiple inputs and outputs:
app.clientside_callback(
'''
function(input1, input2, selected_store) {
return [
input1 + input2,
selected_store.toUpperCase(),
];
}
''',
[
Output('sum', 'children'),
Output('display', 'children'),
],
[
Input('num1', 'value'),
Input('num2', 'value'),
],
Input('store', 'data'), # Can also use State
)Load JavaScript from a file (cleaner for complex logic):
import os
# assets/custom.js
# window.myFunctions = window.myFunctions || {};
# window.myFunctions.toUpperCase = function(val) { return val.toUpperCase(); };
app.clientside_callback(
'''window.myFunctions.toUpperCase''',
Output('output', 'children'),
Input('input', 'value'),
)Place the file in assets/ directory — Dash automatically loads .js and .css files from there.
Debug clientside callbacks with browser console (F12 → Console tab):
# Add debugging to your JavaScript
app.clientside_callback(
'''
function(input_val) {
console.log("Input received:", input_val);
console.log("Type:", typeof input_val);
var result = input_val.toUpperCase();
console.log("Output:", result);
return result;
}
''',
Output('output', 'children'),
Input('input', 'value'),
)Fix 5: DataTable Filtering Not Working
The Dash DataTable supports built-in filtering, but it only works if the data is passed correctly and the filter_action is set.
from dash import dash_table, Input, Output, callback
import pandas as pd
df = pd.DataFrame({
'id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Charlie', 'David'],
'score': [85, 90, 78, 92],
})
app.layout = html.Div([
dash_table.DataTable(
id='datatable',
columns=[{'name': i, 'id': i} for i in df.columns],
data=df.to_dict('records'),
filter_action='native', # Enable built-in filtering
filter_action='custom', # Or use custom filtering via callback
sort_action='native',
page_action='native',
page_size=10,
),
])
# Custom filtering (if filter_action='custom'):
@callback(
Output('datatable', 'data'),
Input('datatable', 'filter_query'), # Filter expression, e.g., "{name} contains 'Alice'"
)
def custom_filter(filter_query):
if not filter_query:
return df.to_dict('records')
# Parse filter_query and apply to df
# Simpler approach: use pandas query
try:
filtered = df.query(filter_query.replace('contains', 'str.contains'))
except:
return df.to_dict('records')
return filtered.to_dict('records')Common mistake — trying to filter before filter_action='native' is set:
# WRONG — no filtering possible without filter_action
dash_table.DataTable(
id='datatable',
columns=[...],
data=df.to_dict('records'),
# filter_action is missing — filtering disabled
)
# CORRECT
dash_table.DataTable(
id='datatable',
columns=[...],
data=df.to_dict('records'),
filter_action='native', # Enable UI filtering
)Editable cells:
dash_table.DataTable(
id='datatable',
columns=[{'name': i, 'id': i} for i in df.columns],
data=df.to_dict('records'),
editable=True, # Allow cell editing
row_deletable=True,
)
# Capture edits via callback
@callback(
Output('datatable', 'data'),
Input('datatable', 'data_timestamp'), # Triggers when data changes
State('datatable', 'data'),
)
def update_data(timestamp, rows):
if not timestamp:
return rows
# rows now contains the edited data
return rowsCommon Mistake: Using filter_action='native' and also trying to filter via a callback — you end up with double filtering or conflicting behavior. Choose one: either native UI filtering or a callback-based custom filter.
Fix 6: Callback Delays and Performance
If callbacks are slow, check whether you’re doing expensive operations synchronously or if you’re missing memoization.
import time
from dash import callback, Input, Output
@callback(
Output('output', 'children'),
Input('button', 'n_clicks'),
)
def expensive_operation(n_clicks):
time.sleep(5) # Blocks the entire callback for 5 seconds
return f"Done! Clicks: {n_clicks}"
# No UI updates until the 5 seconds finishMove expensive operations to @long_callback (Dash 2.2+):
from dash.long_callback import DiskcacheManager
import diskcache
from dash import long_callback, Input, Output
cache = diskcache.Cache("./cache")
long_callback_manager = DiskcacheManager(cache)
app = dash.Dash(__name__, long_callback_manager=long_callback_manager)
@long_callback(
Output('output', 'children'),
Input('button', 'n_clicks'),
running=[
(Output('button', 'disabled'), True, False), # Disable button while running
(Output('progress', 'style'), {'display': 'block'}, {'display': 'none'}),
],
)
def long_running(n_clicks):
time.sleep(5)
return f"Done! Clicks: {n_clicks}"
app.layout = html.Div([
html.Button('Start', id='button'),
html.Div('Working...', id='progress', style={'display': 'none'}),
html.Div(id='output'),
])long_callback shows a loading state and doesn’t freeze the browser while the callback runs.
Use prevent_initial_call=True to avoid running callbacks on page load:
@callback(
Output('graph', 'figure'),
Input('dropdown', 'value'),
prevent_initial_call=True, # Don't run when page loads
)
def update_graph(value):
return go.Figure()Fix 7: State Not Updating in the UI
@callback(
Output('store', 'data'),
Input('button', 'n_clicks'),
)
def store_data(n_clicks):
return {'clicks': n_clicks}
# Display
@callback(
Output('display', 'children'),
Input('store', 'data'),
)
def display_data(data):
return str(data) # May not update if store.data is never inputThe issue: dcc.Store component is for client-side data persistence, not server state. Updates to store.data only trigger callbacks if store is an Input to a callback.
# Correct pattern
app.layout = html.Div([
html.Button('Click', id='button'),
dcc.Store(id='store'), # Stores data on client
html.Div(id='display'),
])
@callback(
Output('store', 'data'),
Input('button', 'n_clicks'),
)
def save_to_store(n_clicks):
return {'clicks': n_clicks}
@callback(
Output('display', 'children'),
Input('store', 'data'), # This callback runs when store updates
)
def display_data(data):
return f"Stored: {data}"Still Not Working?
Dash DataTable Column Resizing Breaks
If column widths reset or resizing is disabled, you may need to set column_selectable or other data table options explicitly:
dash_table.DataTable(
id='datatable',
columns=[...],
data=df.to_dict('records'),
style_data={'width': '150px'},
column_selectable='single',
selected_columns=['id'],
)Asset Files (CSS, JavaScript) Not Loading
Dash loads files from the assets/ folder automatically. If styles or scripts aren’t being applied:
- Check the folder structure — must be
assets/in the project root (same level as where you runapp.py) - Clear browser cache —
Ctrl+Shift+Deletein most browsers - Check browser console (F12 → Console) for 404 errors loading assets
Integrating with Flask or FastAPI
Dash is often embedded in a larger web app. The underlying app.server is a Flask instance, so you can mount Dash inside an existing Flask app or proxy requests from FastAPI. When routing fails, treat it as a Flask problem first — see Flask not working for the routing patterns that apply when you mix frameworks.
Debugging with debug=True
Always run Dash with debug=True during development:
if __name__ == '__main__':
app.run_server(debug=True) # Hot reload, verbose error messagesThis enables hot reloading of code changes and detailed error messages in the browser. Disable debug=True in production for security and performance.
Callbacks Lost After Gunicorn Reload
Gunicorn with --reload watches .py files and restarts the worker on change. But Dash registers callbacks at import time, so a worker mid-callback when the file changes returns an error and the next request rebuilds the callback graph. If you see Callback function not found for output ... after editing code in production-like setups, you’re hot-reloading a worker that’s serving a request. Disable --reload outside development.
502 Bad Gateway from Reverse Proxy
When nginx returns 502 in front of a Dash app, the WSGI worker either crashed or didn’t bind to the expected port. Check Gunicorn logs for stack traces and app.server for binding to 0.0.0.0:$PORT, not localhost. Container health checks must hit a route that doesn’t depend on callbacks — use /_dash-layout (always returns 200 once the app loads) instead of the index. For the general nginx 502 patterns, see nginx 502 bad gateway.
Cookies and Session Persistence Across Deployments
dcc.Store(storage_type='session') survives page reload but is wiped when the browser closes. storage_type='local' persists across closes. On platforms with sticky sessions disabled (Cloud Run, multiple Gunicorn workers without a shared backend), users hitting different replicas see different dcc.Store data only if you use local storage on the client side — server-side state needs a real database. The fix: stop relying on in-memory Python globals; use Redis or Postgres for any data shared across requests.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Plotly Not Working — Figure Not Showing, Kaleido Export Errors, and Subplot Issues
How to fix Plotly errors — figure not rendering in Jupyter, ValueError must install Kaleido, static image export slow or crashing, subplot trace placement, update_layout not working, and offline vs online mode confusion.
Fix: Gradio Not Working — Share Link, Queue Timeout, and Component Errors
How to fix Gradio errors — share link not working, queue timeout, component not updating, Blocks layout mistakes, flagging permission denied, file upload size limit, and HuggingFace Spaces deployment failures.
Fix: Streamlit Not Working — Session State, Cache, and Rerun Problems
How to fix Streamlit errors — session state KeyError state not persisting, @st.cache deprecated migrate to cache_data cache_resource, file upload resetting, slow app loading on every interaction, secrets not loading, and widget rerun loops.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.