Offline-first and sync

What is an optimistic update?

An optimistic update is a change an app shows as if it has already succeeded, before the server or another device confirms it. The app keeps a way back, so if the confirmation fails it can restore the previous state or mark the change as failed and offer a retry.

Learning objectives

After reading this article you will be able to:

  • Describe the usual steps of an optimistic update and its rollback
  • Explain why offline optimistic updates need a persistent queue and visible status
  • Identify actions that should wait for the server instead of updating optimistically

Show it now, confirm it later

In a request-and-wait app, tapping “like” or “mark done” sends a request and the interface changes only when the answer comes back. On a slow network that is a visible pause; with no network it is an error.

An optimistic update reverses the order. The app applies the change on screen at once, sends the request in the background, and deals with the answer when it arrives. When the request succeeds, the person never notices there was a network involved. The work is in handling the cases where the answer is no.

The usual sequence

Client libraries for the web describe nearly the same steps. In TanStack Query’s guide for its cache, the app:

  1. Cancels any refetch in progress, so a late response cannot overwrite the optimistic value.
  2. Takes a snapshot of the current value.
  3. Writes the new value into the cache, which redraws every screen that uses it.
  4. Sends the change to the server.
  5. On error, restores the snapshot. Either way, it refetches afterwards so the screen ends on the server’s real state.

TanStack also documents a lighter variant that leaves the cache alone. While the request is pending, the interface renders the submitted value as a temporary item, for example at reduced opacity, and the item disappears when the request settles. If it fails, the app can keep showing it with a retry button.

Apollo Client keeps the optimistic value apart from the real one. When a mutation starts, its cache stores a separate optimistic version of the object rather than overwriting the existing cached version, so the cached data stays correct if the guess was wrong. When the server responds, Apollo removes the optimistic version and writes the server’s values; if the mutation returns an error, it discards the optimistic version and rolls back to the previous state.

React’s useOptimistic hook works at the component level. It shows a temporary value while an action is in progress, and if the action fails, the interface returns to the value it showed before.

Speculative until confirmed

The server does not have to agree with the guess. It may hold newer data, apply a rule the device does not know, or compute a different result. Replicache’s documentation calls client-side results speculative and server results canonical: changes run locally first, are pushed to the server and run again there, and the server’s result takes precedence when the client pulls it back. To combine the two, Replicache rewinds to the last state it received from the server, applies the server’s changes, and then replays any changes still pending on top.

This is the general shape of reconciliation. The confirmed state is the base, and pending local changes are reapplied on top of it, so the person still sees their unconfirmed work.

What changes when the app is offline

On a working connection an optimistic update lasts only as long as one request. In an offline-first app it can last for days, which raises the stakes:

  • The pending change must survive a restart. Keep it in a persistent queue, an outbox, not only in memory. Android’s guide calls this pattern a lazy write: write to the local store first, then queue the change for the network.
  • Retries must be safe. A change sent, interrupted, and sent again must apply once, which needs a stable identifier per change.
  • Other edits arrive meanwhile. By the time the change is sent, someone else may have changed the same record, so the app needs a rule for sync conflicts.
  • Status must be visible. A change shown as done for a day and then rejected is worse than a short spinner, unless the person could see it was pending. Offline Protocol’s documentation recommends separate indicators for locally saved work, peer delivery, application acceptance, and backend commit, and a recovery action when something is rejected. Delivered, accepted, committed explains why these differ.

When not to be optimistic

Some actions should wait for an answer. Android’s guide gives a bank transfer as an example of an online-only write: the app should disable the action offline or tell the person it failed, not pretend it worked. The same goes for claiming the last seat, changing permissions, or anything where a later reversal would mislead someone who has already acted on it.

A useful test is to ask what happens if the change is rejected an hour after the person saw it succeed. If the honest answer is a short note and a retry, an optimistic update is fine. If the answer is a missed flight or a double sale, wait for the server.

Frequently asked questions

What is the opposite of an optimistic update?

A pessimistic update, where the interface waits for the server to confirm before showing the change. It is simpler and never shows a change the server has not accepted, but every action shows a wait, and nothing works offline.

Do optimistic updates cause data loss?

Not by themselves. The risk is a change that looks saved but is later rejected or lost from an in-memory queue. Persist pending changes and show when something has not been confirmed.

Sources

Build it with Offline Protocol

The application state page in the Offline Protocol docs explains how to show locally saved work, peer delivery, application acceptance, and backend commit as separate states, with a recovery action for rejections.

Read application state and receipts