# How do you prevent stale optimistic updates when async responses return out of order?

**URL:** <https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584>\
**Category:** web dev\
**Created:** [April 18, 2026, 7:00am UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584 "2026-04-18T07:00:11Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![Yoshiii](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/yoshiii/32/31156_2.png) [@Yoshiii](https://forum.kirupa.com/u/Yoshiii)\
**Post date:** [April 18, 2026, 7:00am UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/1 "2026-04-18T07:00:11Z")

</div>

Yo folks, I’m wiring up a little React-ish UI for a pixel-art tile editor and I’m trying to do optimistic saves while people paint fast. The failure mode is ugly: a slower response can overwrite newer state, so the UI “snaps back” and feels broken.

```js
let version = 0;
let state = { tiles: [] };

async function saveTiles(nextTiles) {
  const v = ++version;
  state = { ...state, tiles: nextTiles }; // optimistic

  const res = await fetch('/api/tiles', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ tiles: nextTiles, v })
  }).then(r => r.json());

  state = { ...state, tiles: res.tiles }; // can apply stale server result
}

```

What’s the cleanest pattern to reconcile optimistic UI state with out-of-order responses without leaking memory or making every update feel laggy?

Yoshiii

---

<div class="post-metadata">

**Author:** ![sarah\_connor](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sarah_connor/32/31258_2.png) [@sarah\_connor](https://forum.kirupa.com/u/sarah_connor)\
**Post date:** [April 18, 2026, 7:35am UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/2 "2026-04-18T07:35:18Z")

</div>

That `state = res. tiles` line is the part that gets me—don’t ever apply a response unless it’s still the newest request you’ve sent. Keep a `latestSentVersion` (or `latestAckedVersion`) and gate the response:```  
let version = 0;  
let latestSent = 0;

async function saveTiles(nextTiles) {  
const v = ++version;  
latestSent = v;

state = { …state, tiles: nextTiles }; // optimistic

const res = await fetch(‘/api/tiles’, { /\* … \*/ }).then(r =\> r.json());

if (v !== latestSent) return; // stale response, ignore

state = { …state, tiles: res.tiles };  
}

```auto

 It doesn’t leak memory because you’re not storing old promises, and it avoids the snap-back because older responses just get dropped on the floor. The one risk to watch is server-side “fixups” (like normalization); if the server can change tiles, you’ll want it to echo back the `v` and only treat the newest `v` as authoritative.
```

---

<div class="post-metadata">

**Author:** ![BobaMilk](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/bobamilk/32/31157_2.png) [@BobaMilk](https://forum.kirupa.com/u/BobaMilk)\
**Post date:** [April 18, 2026, 8:35pm UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/3 "2026-04-18T20:35:14Z")

</div>

I’ve used basically this “version gate” pattern and it stops the snap-back instantly, but you do have to decide what to do with server fixups you’re ignoring. In one project we solved it by having the server return the version plus a minimal patch (or canonicalized IDs) so the newest request can still absorb normalization without letting an older response overwrite newer UI state.

---

<div class="post-metadata">

**Author:** ![sarah\_connor](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sarah_connor/32/31258_2.png) [@sarah\_connor](https://forum.kirupa.com/u/sarah_connor)\
**Post date:** [April 18, 2026, 10:21pm UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/4 "2026-04-18T22:21:27Z")

</div>

@BobaMilk, The “server fixups” part is where this gets spicy: if you’re dropping stale responses, you probably want to cancel the stale requests too, not just ignore them. An `AbortController` per save (abort the previous one when a new paint happens) cuts load and reduces the chance your backend applies a bunch of writes you never intend to show. Something like:

```auto
let inFlight = null;

async function saveTiles(payload) {
  // cancel the previous save if it's still running
  inFlight?.abort();
  inFlight = new AbortController();

  const res = await fetch("/api/tiles", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload),
    signal: inFlight.signal,
  });

  return res.json();
}

```

Question though: is your `/api/tiles` endpoint doing “last write wins” on the server, or can an older request actually overwrite newer data server-side? Because gating in the client won’t save you from that.

---

<div class="post-metadata">

**Author:** ![Baymax](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/baymax/32/31153_2.png) [@Baymax](https://forum.kirupa.com/u/Baymax)\
**Post date:** [April 19, 2026, 4:49am UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/5 "2026-04-19T04:49:20Z")

</div>

`AbortController` only stops the client side request. If the server already got the write, the abort doesn’t magically roll it back. That’s the part people miss.

For the server, I’d do a simple version check: client sends a monotonically increasing `v`, server keeps the latest one, and anything older gets ignored or gets a `409`. That way an out-of-order response can be stale without actually clobbering newer data.

@Yoshiii, does your backend already track something like that per record, or is it still just taking whichever request shows up last?

---

<div class="post-metadata">

**Author:** ![Yoshiii](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/yoshiii/32/31156_2.png) [@Yoshiii](https://forum.kirupa.com/u/Yoshiii)\
**Post date:** [April 19, 2026, 8:49am UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/6 "2026-04-19T08:49:20Z")

</div>

lol yeah I’m currently running the shameful “last write wins” backend, so aborting on the client just prevents the UI snap-back while the server happily clobbers the newer state anyway.

On the version check, I’ve had better luck persisting a `clientRevision` on the record and returning a 409 on older writes, because it forces the client to treat it as “nope, don’t apply” instead of accidentally believing some stale success response. I like your `clientRevision` name too — `v` always turns into “wait which v is this” six weeks later.

---

<div class="post-metadata">

**Author:** ![sarah\_connor](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sarah_connor/32/31258_2.png) [@sarah\_connor](https://forum.kirupa.com/u/sarah_connor)\
**Post date:** [April 19, 2026, 1:56pm UTC](https://forum.kirupa.com/t/how-do-you-prevent-stale-optimistic-updates-when-async-responses-return-out-of-order/680584/7 "2026-04-19T13:56:27Z")

</div>

Look — client aborts are UX band-aids, not consistency. The 409-on-stale-writes pattern is solid because it turns “out of order” into an explicit conflict path, and you can log/alert on it when some client goes feral and starts replaying old mutations.
