Skip to content

Confusing 'Duplicate callback outputs' error when allow_duplicate=True collides on Input-derived hash, not documented anywhere #3901

Description

@biyani701

Describe your context

dash==4.3.0
Python 3.14

Describe the bug

When two (or more) callbacks legitimately write the same Output with allow_duplicate=True and prevent_initial_call=True set correctly on every writer (per the Dash duplicate-outputs docs), the browser can still raise:

Duplicate callback outputs
In the callback for output(s):
  some-id.is_open
  some-id.data
  some-id.children@<hash>
Output 2 (some-id.children@<hash>) is already in use.
To resolve this, set `allow_duplicate=True` on
duplicate outputs, or combine the outputs into
one callback function, distinguishing the trigger
by using `dash.callback_context` if necessary.

The message is actively misleading in this situation: it tells you to set allow_duplicate=True and mentions nothing else diagnosable, but every writer of the output already has allow_duplicate=True and prevent_initial_call=True. There is no indication of why the check still failed, and no information identifying which other callback (function name / file / registration) the conflict is actually with.

Root cause (found by reading source)

create_callback_id() in dash/_utils.py builds the frontend-facing dedup key for an allow_duplicate=True output as:

def _concat(x):
    nonlocal hashed_inputs
    _id = x.component_id_str().replace(".", "\.") + "." + x.component_property
    if x.allow_duplicate:
        if not hashed_inputs:
            hashed_inputs = _hash_inputs()   # sha256(".".join(str(x) for x in inputs))
        _id += f"@{hashed_inputs}"
    return _id

i.e. the @<hash> suffix that makes two allow_duplicate writers of the same output distinguishable is derived only from that callback's Input list (confirmed: State is excluded — register_callback() passes flat_inputs and flat_state to insert_callback()/create_callback_id() as separate args, and only flat_inputs feeds the hash).

The browser-side check that raises the error (dash-renderer's findDuplicateOutputs, in dash/dash-renderer/build/dash_renderer.dev.js) then just does a outStrs[idProp] lookup against this literal id.property@hash string — it has no way to explain why two registrations produced the same hash, nor which callback the other one is.

This means the "already in use" collision can happen in at least two scenarios that have nothing to do with the two flags the error message points at:

  1. Coincidental Input-hash reuse — two different callbacks intentionally sharing the same Output (the documented use case for allow_duplicate) happen to also be triggered by the exact same Input list, producing identical hashes even though the outputs/behavior are otherwise unrelated.
  2. Accidental double-registration — the same callback module gets imported/executed twice in one process (dev-server hot reload, duplicate import path, etc.), which trivially produces an identical Input hash against itself. This is arguably a "real" duplicate, but the error gives no way to tell it apart from case 1, or from a genuine third-callback conflict.

Neither of these is discoverable from the error text or the allow_duplicate docs, which describe allow_duplicate=True + prevent_initial_call=True as sufficient. We only found the actual mechanism by reading dash/_utils.py and the renderer bundle directly.

Expected behavior

At minimum, one of:

  • The error message should say why the collision key matched (e.g. "this output's dedup key also depends on your callback's Input list — two callbacks with the same Inputs writing this Output will collide even with allow_duplicate=True") instead of only repeating the two flags that are already presumably set.
  • The error should identify the other callback involved in the collision (function name, output list, or at least its full dependency signature) so the conflict is actually locatable in a large app.
  • The allow_duplicate documentation should mention that the dedup mechanism is Input-derived, since that's a real constraint on when two duplicate-writers can safely coexist.

Reproducible example

We don't have a minimal isolated repro of scenario (1) above handy (our real occurrence was a 3000+ line callback module), but the mechanism itself is directly reproducible by unit-testing dash._utils.create_callback_id with two Output objects (allow_duplicate=True) plus two different inputs lists that happen to stringify identically, or two calls with the same inputs list — both produce identical @hash suffixes. Happy to put together a full minimal Dash app repro if useful — wanted to file this first since the underlying mechanism was straightforward to pin down from source.

Workaround

Merging the conflicting callbacks into a single callback function and dispatching on dash.callback_context.triggered_id (as the error text itself suggests) avoids the issue entirely, since there's then only one registration of the output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions