---
title: Optimistic updates
description: Make writes feel instant by declaring one transform per query, with a check that catches the one you forgot.
order: 3
---

# Optimistic updates

When someone clicks "Add to cart," you want the badge to tick up immediately instead of after a
round-trip. You declare one transform for the cart query that predicts the new count, and every
island showing that query updates from it — including islands you write months later. You don't
wire per-component optimistic state, and there's no cache to patch. A check tells you the moment you
forget a transform.

## Declare transforms

A predictor is a pure `(current, input) => next` function over one query's result type. Extract it
for direct testing, bind it through that query handle, and put the binding on the mutation next to
the write it predicts:

```ts
export const predictCart = (current: Readonly<{ count: number }>, input: { quantity: number }) => ({
  count: current.count + input.quantity,
});

export const addToCart = app.mutation({
  input: addToCartInput,
  optimistic: [
    cartQuery.optimistic(addToCartInput, predictCart),
    productGridQuery.optimistic('await-fragment'),
  ],
  queue: queue('cart'),
});
```

The surrounding access decision, invalidation registry, and handler are omitted here. Three things
to notice:

- **The identities are query handles, not strings.** `cartQuery.optimistic(...)` carries the exact
  result type, app ownership, and input-schema identity. The compiler resolves the handle to wire
  identity and compares the bindings with the mutation's derived invalidation set. Add a write that
  touches a new domain and `kovo check optimistic` reports the uncovered query.
- **`'await-fragment'` is a real answer.** It says "considered; the 1-RTT latency is fine here" — the
  product grid re-renders from the server fragment instead of being predicted. A deliberate deferral
  and a forgotten transform are different states, and only the second one is a diagnostic.
- **`queue('cart')`** gives related cart writes one named FIFO queue. Submissions sharing a queue still apply
  their optimistic transforms immediately, in submit order; only the network request waits behind the
  queue head. Two quick "add" clicks therefore show both predicted increments right away while the
  requests settle in order.

Predictors receive a read-only view and return the next value. A column rename breaks the predictor
in the editor instead of in production. The standalone string-keyed plan shape is compiler-owned
generated ABI; app source has one authoring path: query-handle bindings inside `app.mutation`.

## Catch a missing transform

Coverage is the invalidated-query set checked against the declared status, per mutation. The valid
v1 statuses are `hand-written`, `derived`, and `await-fragment`; anything else is a diagnostic. The
check runs from the same derived set in `kovo check` and in editor-visible registry typing.

Here is the failure, from running `kovo check` against the commerce graph with the `cart` transform
uncovered:

```txt
kovo-check/v1
WARN KV310 cart/add -> cart Invalidated query lacks optimistic transform.
```

`kovo check` exits non-zero on that warning, so the gap can't ship silently. The matching
`kovo explain mutation cart/add --optimistic` run shows the same gap inline with its fix menu:

```txt
OPTIMISTIC productGrid await-fragment
OPTIMISTIC orderHistory await-fragment
OPTIMISTIC cart UNHANDLED
  -> hand-write in the mutation module, or declare 'await-fragment'
OPTIMISTIC-SUMMARY total=3 derived=0 hand-written=0 await-fragment=2 UNHANDLED=1 PUNTED=0
```

With the transform in place, the same command reports clean coverage:
`OPTIMISTIC-SUMMARY ... UNHANDLED=0`, with one `OPTIMISTIC` line per invalidated query. The full
annotated artifact lives in [reading kovo check & kovo explain](/guides/kovo-explain/#read-the-output).

## Run it

Open two tabs on the same page. Submit the mutation in tab A under throttled network conditions.
You should see the optimistic state land immediately in A, then settle to server truth when the
response arrives. Tab B stays unchanged until it gets real server truth from its own freshness path.

A forgotten optimistic update is a visible, suppressible diagnostic with the suppression recorded in
source. The same check runs one hop further down: every query-dependent DOM position needs a
declared update status too.

## What the runtime does on submit

When the user submits, the loader runs a fixed sequence:

1. **Snapshot** the affected query values under structural sharing. The runtime copies only the
   touched paths (copy-on-write) and keeps untouched subtrees by reference; `JsonValue` makes the
   values safe to snapshot and restore, but the contract is not an unconditional deep clone.
2. **Apply transforms** to the shared query values and run their update plans. Every dependent island
   updates at once.
3. **Stamp pending state.** Affected islands get `kovo-pending` and `aria-busy="true"` automatically,
   so you style the in-flight state with CSS and wire no per-component spinner:

```html
<cart-badge kovo-deps="cart" kovo-pending aria-busy="true">…</cart-badge>
```

4. **On success**, the response's `<kovo-query>` values and fragments reconcile over the prediction. A
   right guess is a near-no-op morph; a wrong guess is corrected silently. Server truth always wins,
   because predictions are throwaway sketches.
5. **On error**, the snapshots are restored and the typed error fragment renders. See the
   [mutations guide](/guides/mutations/) for the 422 path.

If server truth never arrives for an applied transform, the client discards the prediction. The
affected query rolls back to its pre-transform snapshot or refetches from `/_q/<key>`; it does not
keep the optimistic value on screen as settled data. Fragment-only responses are allowed when you
declared `'await-fragment'`, but silent inconsistency is not.

## Rebase concurrent mutations

Each query keeps a pending-transform log. When server truth arrives while other mutations are still
in flight, the runtime morphs the authoritative value in, then re-applies the still-pending
transforms in order. This rebase is safe because predictors are pure value transforms:

```ts
// Conceptually, the loader keeps a per-query pending-transform log.
pending.add('m1', { productId: 'p1', quantity: 2 }, predictCart); // predict
pending.applyServerTruth('cart', { count: 7 }); // morph truth in, re-apply pending
pending.settle('m1'); // m1's response landed
```

Navigation reconciles for free: in-flight mutations complete via `keepalive`, and the pending log
dies with the document, so stale optimism can't outlive its mutation.

## Test the prediction

A predictor is an ordinary pure function, so its unit test needs no framework plan or draft adapter:

```ts
expect(predictCart({ count: 1 }, { productId: 'p1', quantity: 2 })).toEqual({ count: 3 });
```

Beyond a point check, you can property-test that the prediction is _contained in eventual truth_ over
generated states — the commuting diagram `patch(shape(s), input) ≡ shape(apply(effect, s, input))`.
That's `propertyTest`, and it lives in the
[testing guide](/guides/testing/#property-test-optimistic-transforms) along with the harness it runs in.

## Derived, mixed, and punted coverage

Kovo supports a spectrum rather than a single optimistic style:

- **Derived** transforms are compiler-emitted from writes whose dataflow is closed over mutation
  input, schema constants, and data the query already ships. The StackOverflow example uses this
  shape for votes and answers.
- **Hand-written** transforms are still the right answer when the product rule is the important part
  of the UI. The CRM example uses this for dashboard summaries whose visible update is clearer as
  domain code.
- **`'await-fragment'`** is an explicit punt to server truth. Use it when totals, rankings,
  inventory, or authorization-sensitive output should not be guessed.

`kovo explain mutation <name> --optimistic` prints one row per invalidated query with the status
`derived`, `hand-written`, or `await-fragment`. When derivation cannot prove a transform, the output
names the expression and the reason for the punt so the choice can be reviewed in code.

### Derivation grammar

The compiler derives transforms by pushing symbolic write effects through query shapes:

```text
Stage 1  write  ->  symbolic row-effects
         value ::= Param(path) | Const | ColRef(t.c) | Arith(op,v,v) | Opaque
         effect ::= INSERT{vals} | UPDATE{match, sets} | DELETE{match} | UPSERT{...}
         (match = eq-predicates on keys; ranges/server-time => Opaque match => punt)

Stage 2  query  ->  shape mapping
         field ::= Scalar(keyed row col) | COUNT(R[, pred]) | SUM(R, arith)
                 | AGG(R, projection)    where R = rowset(filter chain, key, orderBy)

Stage 3  push effect through shape  ->  JSON-patch program over client data
         INSERT x AGG   => push (defaults from schema; Opaque cols => tempId()/now()
                          placeholders, pending-styled, content-matched on reconcile;
                          orderBy decides insertion point; Opaque orderBy col => punt)
         UPSERT x AGG   => find-then-update-else-push (branchiness reproduced client-side)
         DELETE x COUNT => -(matched count, computable iff client holds rows)
         DELETE x SUM   => -SUM(contribution) iff query also ships the rows; else punt
         SET on filtered col => membership transition: Const vs filter => exit derivable,
                                entry punts because the client lacks the row's other columns
         row possibly outside client's rowset => emit guard (find-or-no-op), not punt
```

Punts are all-or-nothing per field. Wrong predictions are worse than none, so these cases stay
explicit: opaque `SET` expressions such as SQL functions, subqueries, or server computation;
non-key match predicates; window functions; `GROUP BY` plus `HAVING`; `DISTINCT`;
interprocedural opacity such as external packages receiving `db`; and params untraceable to
mutation input or a session key.

## When not to predict

Reach for `'await-fragment'` more often than SPA habits suggest:

- The server computes something the client can't — totals with tax rules, ranking, inventory races.
  A wrong prediction is worse than a 1-RTT wait.
- The query feeds a `fragment`-status region. Fragment positions get no optimistic update by
  definition; the server re-render is the update.
- The data is rarely on screen during the mutation.

Declare the deferral and move on. The check is satisfied either way.

## Next

- [Mutations & forms](/guides/mutations/) — the round-trip these transforms predict.
- [Reading kovo check & kovo explain](/guides/kovo-explain/) — the coverage checks in CI.

<details>
<summary>Spec & diagnostics</summary>

Optimism keyed to queries, the runtime protocol, immediate optimistic application with serialized
requests for queued mutations, bounded structural-sharing snapshots, rebase, and navigation
reconciliation: SPEC §10.4.
Derived transforms and explicit punts: SPEC §10.5. The coverage check at both altitudes: SPEC §10.6;
the emitted invalidation sets it reads: SPEC §6.1. A missing optimistic transform is **KV310**;
anything other than `hand-written`/`await-fragment` is the same code. Every query-dependent DOM
position needing a declared update status is **KV311** (SPEC §4.9). Fragment-status positions:
SPEC §4.9. Missing server truth for an applied transform is **KV313**; SPEC §10.4 requires the
runtime to discard or refetch the prediction rather than freeze it. Navigation and `keepalive`:
SPEC §8. Property-testing transforms: SPEC §12.

API reference: [@kovojs/server](/api/server/).

</details>
