Description
Describe the bug
On a page with many server callbacks firing at load, a URL-to-component sync set up with two clientside callbacks goes into an infinite loop on 4.5.0rc0. It works on 4.4.1.
readUrl: Inputurl.search,prevent_initial_call="initial_duplicate". It callsdash_clientside.set_props("tabs", {"value": <tab from ?tab=>}).tabSearch: Inputtabs.value, Stateurl.search, Outputurl.search(allow_duplicate=True),prevent_initial_call=True. It writes?tab=<value>back to the URL.
Loading /page?plan=21&tab=history on 4.5.0rc0 gives this trace (store = the tabs.value in window.store):
1397ms store tabs.value undefined -> "scope" (layout default)
1748ms readUrl: set_props("tabs", {value: "history"}) ; store -> "history"
1759ms tabSearch runs, triggered [{"prop_id":"tabs.value","value":"scope"}] <-- stale value
1934ms tabSearch runs, triggered [{"prop_id":"tabs.value","value":"history"}]
... the stale run writes ?plan=21, readUrl sets the tab back to "scope", tabSearch gets "history"... forever
tabSearch runs twice, and the first run gets the layout's stale value even though the store already holds the value set_props wrote. On 4.4.1 it runs once, with "history".
Bisected to the renderer change in observers/prioritizedCallbacks.ts / observers/requestSlot.ts (from #3931, commit ce7530c), which exempts clientside callbacks from the 12-slot HTTP budget. With only !cb.callback.clientside_function && removed from usesRequestSlot in the built dash_renderer.min.js, so clientside callbacks count toward the budget again as in 4.4.1, the same page settles: one request after load, ?tab=history kept, History tab selected. With the stock 4.5.0rc0 renderer the page loops until the browser tab crashes.
The CHANGELOG entry for #3931 doesn't mention the scheduling change.
Repro status
Our reduced app (dmc.Tabs + the two callbacks above, a router callback returning the page, 20 slow server callbacks on load) did NOT reproduce the 4.4.1/4.5 difference, so the exact extra condition is still unknown. The evidence above comes from the full app, where swapping only that renderer line toggles the bug. Happy to test a candidate fix.
Environment: dash 4.5.0rc0 (FastAPI backend), dash-mantine-components 2.8.0, React 18.3.1 (default), Chrome stable (Playwright), Python 3.14.
Workaround: guard the URL writer against its own echo (ignore tabs.value events until the value set_props wrote comes back).
Activity
camdecoster commentedon Sep 28, 2026
Thanks for the report! This will probably be hard to diagnose without reproduction steps. Would you be able to take another attempt at creating a "reduced app" that shows this problem?
biyani701 commentedon Sep 29, 2026
Here's a reduced app that reproduces it. It's standalone: only
dashis needed, no DMC or FastAPI.Correction to my original report: this isn't really a new bug in 4.5. The same stale read happens on 4.4.1 once the page isn't under load. In 4.4.1, the 12-slot HTTP budget happened to hide it, and #3931 removes that cover for clientside callbacks.
app.py
Steps
pip install dash==4.5.0rc0and runpython app.py.http://127.0.0.1:8050/?tab=band watch the console.Expected: URL stays
?tab=band tab B is selected.Actual (4.5.0rc0):
readUrlandtabSearchping-pong until the loop brake intabSearchkicks in (without the brake the browser tab eventually crashes). The URL ends up empty and tab A is selected:Results (Chrome, headless via Playwright)
N_SLOW=20(default)N_SLOW=0?tab=b, B selectedThe stale
tabSearch tab=arun shows up in every cell, including 4.4.1 withN_SLOW=20. There, it's just that the later runs happen to converge.What seems to be going on
tabsis inserted inside a chunk returned by the router callback, and itsOutput(url.search) sits outside that chunk. SotabSearchis triggered by the insertion even though it hasprevent_initial_call=True, which is documented behavior.readUrlmakes its initial call at the same time. Both are clientside and ready, so the prioritized-callbacks observer picks them in the same batch. Every callback in the batch is then executed against thelayoutsnapshot the observer read at the start:readUrlruns first and itsset_propsupdates the store.tabSearchthen runsfillValsagainst the old snapshot and gets"a".set_propsalso re-triggerstabSearch, which then runs a second time with"b". That's why it's seen twice.In 4.4.1 under load, the 12-slot budget meant
tabSearchusually landed in a later observer pass, afterset_props, so it read a fresh layout. In 4.5 clientside callbacks are exempt from the budget, so they now always share the batch.Candidate fix (tested locally)
Read the layout fresh for each callback instead of using the observer's snapshot. In the built
dash_renderer.dev.jsI made this one-line change in the sync branch:With that change, 4.5.0rc0 settles in both columns: 3 runs, no stale value,
?tab=bkept.pathsis probably worth reading fresh as well, and the deferred/async branch passes the same capturedlayout, so it may need the same change.For comparison, putting clientside callbacks back under the budget (removing
!cb.callback.clientside_function &&fromusesRequestSlot) only fixes theN_SLOW=20case.N_SLOW=0still loops, same as 4.4.1.Environment: Chrome 153, Python 3.12, Flask backend (also seen with FastAPI in the full app).
camdecoster commentedon Sep 29, 2026
Thanks for that! Our team will take a look.