Skip to content
LaravelBlog

livewire #10513: [4.x] Discard stale live model responses after newer responses land

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Livewire 4 includes a fix for one of the most long-standing and confusing wire:model.live quirks: responses arriving out of order and rolling your UI backward. PR #10513 now discards stale live-model responses once a newer one has landed, so the state on screen always corresponds to your most recent input. This race has been reported in #8341, #2480, and #9030, and this PR is an alternative implementation to #10488. The change lives entirely inside Livewire's JavaScript request pipeline — there are no API changes — so upgrading from v3.8.10 to Livewire 4 picks it up automatically.

Why wire:model.live responses could arrive out of order

Livewire intentionally allows wire:model.live requests to overlap, so fast typing doesn't wait for each previous request to finish. That means responses can also finish out of order:

  1. You type j — request A ("j") goes out.
  2. You type k — request B ("jk") goes out without waiting for A.
  3. B's response arrives first and renders jk.
  4. A's older response arrives afterward and merges its stale snapshot and effects.

The result: your input and server-rendered state visibly roll backward from jk to j. For example, with a search component like this, the results list could briefly (or permanently) match text you already typed over:

<input type="search" wire:model.live.debounce.150ms="query">

<ul>
    @foreach (Post::search($query)->limit(10)->get() as $post)
        <li>{{ $post->title }}</li>
    @endforeach
</ul>

Simply aborting the older HTTP request isn't safe either, because a single Livewire request can carry messages for multiple components — aborting could discard unrelated work.

How the fix works

The model.live fast path in interactions.js is the one place where a new action is intentionally allowed to overlap active messages in the same request scope. With this change, the incoming action captures the active messages it will supersede — but only messages whose actions are all model.live — into a new action.supersedes set.

Crucially, sending the newer request does not immediately invalidate the older one. The older message is only stamped superseded = true when the newer response is actually accepted, inside request/index.js. If an older response then lands, discardIfSuperseded() settles it — resolving its action promises and invoking its finish lifecycle — but skips merging its snapshot and running its effects.

This keeps the rule local: no global counters, no WeakMap ordering registry, and component and island scopes stay independent through the existing message-bus scope matching. Because every active older live-model message is captured, three or more overlapping requests behave exactly like two.

What the fix guarantees (and doesn't)

The delayed stamping is the key design decision. If the newer request is cancelled or fails at the network or HTTP layer, the older successful response can still land — so a flaky connection can't lose your updates. Two related guarantees:

  • Skipped payloads count. When the server responds with a "skip" payload (unchanged reactive props), that still counts as an accepted response and stamps older messages. Otherwise an older snapshot could still roll the component backward.
  • Nothing else changes. Mixed and non-live-model messages are never stamped, and existing bound-child behavior remains serialized, so wire:model and wire:click flows behave as before.

The only observable difference: effects from a superseded .live response no longer run, and loading states still resolve normally because the message lifecycle always finishes.

How it's verified

A new browser test, LiveModelResponseOrderingBrowserTest, reproduces the j / jk failure deterministically. It patches window.fetch to hold the first response for 500ms while the second one renders, then releases the stale response and asserts that both the input and the server-rendered value remain jk. The test fails on unchanged 4.x (the rendered value rolls back) and passes with this PR:

php vendor/bin/phpunit --configuration phpunit.xml.dist src/Tests/LiveModelResponseOrderingBrowserTest.php

Upgrading from v3.8.10

  • The fix ships in Livewire 4.x only. v3.8.10 still exhibits the rollback race; upgrading to Livewire 4 resolves it with zero code changes.
  • No public API changes. The new pieces (Action.supersedes, Message.superseded, MessageBus::activeMessagesMatchingScope) are internal.
  • You can drop workarounds. Extra-long debounce values or custom fetch interception added to mask stale responses are no longer needed.
  • Staying on v3? Increase your .debounce duration to reduce the window, but note the race itself remains unfixed there.

Takeaways

  • Out-of-order wire:model.live responses no longer roll your input or server-rendered state backward; the newest accepted response always wins.
  • Older messages are only invalidated when a newer response is accepted — failed or cancelled newer requests still let older responses land.
  • Skipped ("unchanged") server payloads count as accepted responses too.
  • Loading states and lifecycle hooks for superseded requests still resolve; only their snapshots and effects are discarded.
  • No API changes — upgrading from v3.8.10 to Livewire 4 gets this behavior automatically.

Categories: 3 (Fix), 11 (Upgrade notes)

Sources

More from this release

Related Articles