The message said the record was created. It was not. A different message said the save had failed. That one was already in the database. The interface was wrong in both directions on the same afternoon.
Optimistic UI is any interface that reports an outcome before the underlying operation has confirmed it, so the message can be right, early, or simply wrong.
Table of Contents
The symptom: successes logged as failures
An automated job created records one at a time and recorded the result of each. Partway through, the log filled with failures — a run of creates that, by the message the interface returned, had not worked.
Except the records were there. Checking the destination directly, several of the “failed” creations existed, complete and correct. The job had done the work and then misread the outcome.
That is the worst version of this bug, because of what happens next. The natural response to a batch of failures is to run it again. Re-running would have attempted to create records that already existed — and depending on how the store handles that, either errored on every one or, worse, silently duplicated them.
A record logged as failed but present in the database is a duplicate waiting for the re-run.
Step 1 — “The creations really did fail”
The first reading is the literal one: the log says failure, so the work failed. It is the reading that triggers a re-run, and it is the one worth disproving before you act on it.
Disproving it took one direct read of the destination. The records the log called failures were present. Whatever the job had recorded, it was not the state of the system.
Step 2 — “Then the log is just wrong”
If the records exist and the log says otherwise, maybe the logging is buggy — writing “failure” where it means “success.” That would be annoying but harmless.
It was not quite that. The job was faithfully recording what the interface handed it. The interface was handing it the wrong thing. The bug was not in the log; it was in what the log trusted.
Step 3 — Find what the code was actually reading
The integration decided success or failure by reading the message the form surfaced after a save — a toast, the kind of transient banner that says “created” or names a problem. That is a reasonable-looking design and a fragile one, because a form surfaces messages for more than one reason.
The form had been redesigned. Its new version fired inline validation messages — “this field is required”, shown against a field — through the same surface the integration was watching for the save result. The code could not tell “the save failed” from “a field lit up briefly during the save,” so a successful create that flashed an incidental validation message was read as a failure.
The message was real. It just was not the message the code thought it was reading.
The interface was telling the truth; the code was reading the wrong sentence.
Step 4 — The probe: ask the system of record, not the screen
The resolution was to stop reading the screen for a fact the screen does not authoritatively hold. After each create, the job now queries the destination directly — does a record with this identity exist? — and treats that answer as the outcome. The toast is ignored for the success decision.
The moment the check moved from the message to the store, the false failures disappeared, because they had never been failures. The records were there; the authoritative read said so; the toast was demoted to what it always was — a hint.
Ask the store what is true; ask the screen only what it was trying to do.
Why an interface message is only ever a hint
A message is generated by the layer you are looking at, at the moment it renders, about what that layer believes. Between that belief and the durable truth sit every way software drifts from its own optimism: a write that is still in flight, a validation banner that shares a channel with a result, a replicated store that has not caught up, a success animation played before the server answered.
None of that makes interfaces dishonest. It makes them the wrong authority for a question about persisted state. The screen answers “what did I just try, and how do I feel about it?” The system of record answers “what is true now.” Only the second question matters when you are about to re-run a job or tell a user their data is safe — the same reason a careful automated job treats a green check as a claim to verify rather than a fact.
The fix, and the discipline around it
Read back after write. The core move: after a create or update, fetch the thing from the authoritative store and confirm it matches. Make that read — not the UI response — the input to your success/failure logic.
Key your check on identity, not position. Confirm by a stable identifier (the record exists with this id), not by scraping the same surface that misled you. A read that goes through the same fragile channel inherits the same lie.
Make re-runs safe anyway. Read-back reduces false failures; idempotent creation removes the damage when one slips through. If creating an existing record is a no-op or a skip rather than a duplicate, a mistaken re-run costs nothing, so a false failure that slips through does no damage.
Log the source of your verdict. Record why you called something success or failure — “confirmed present in store” versus “toast said created.” When the two disagree later, that line tells you which to believe.
What read-back cannot do
Reading back proves the record exists; it does not prove it is correct, and it costs a round trip per item. For a create it is close to definitive — present or absent is the whole question. For an update it is weaker: the record can exist and still hold the wrong value, so the read-back must compare fields, not just existence. And it does not fix the interface; the toast will keep lying. Read-back is a discipline for the consumer of an unreliable signal, not a repair of the signal — which is why it pairs with idempotent writes rather than replacing them.
The short version
An integration decided success by reading a form’s toast message. After the form was redesigned, inline validation messages (“field required”) flowed through the same surface as the save result, so genuinely created records were logged as failures — and a re-run would have duplicated them. The interface was truthful; the code was reading the wrong message. The fix is to verify against the system of record: after each write, read the record back by identity and treat that as the outcome, ignoring the toast for the success decision. A message tells you what the software intended; only the authoritative store tells you what is real — so read back after write, key on identity, keep writes idempotent, and log which source your verdict came from.
Frequently Asked Questions
Why should I not trust a “success” message from a form or API?
Because the message reports what the software attempted and how the presentation layer interpreted it, not the durable state of the system of record. A write can still be in flight, a validation banner can share the same channel as a result, or an optimistic interface can show “saved” before the server confirms. The message is a useful hint and a poor authority.
How do I verify a record was actually created?
Read it back from the authoritative store, keyed on a stable identifier, and treat that read as the outcome. For a create, existence is the whole question. For an update, compare the fields you changed, because a record can exist and still hold the old value. Make this read the input to your success logic instead of the UI response.
What is optimistic UI and why does it cause false success?
Optimistic UI updates the screen to show an operation as done before the underlying write has confirmed, to feel responsive. It is a good user-experience choice and a bad source of truth: if the write later fails, the interface has already claimed success. For anything that drives a decision — a re-run, a “your data is safe” — confirm against the store rather than the optimistic view.
Why is a false failure more dangerous than a false success?
Because the natural response to a failure is to retry, and retrying an operation that actually succeeded can create duplicates in the system of record. A false success leaves you missing data you think you have — bad, but usually caught later, unlike a genuine loss you must distinguish from a bad read. A false failure actively invites you to double the data, which is harder to clean up than either single mistake.
How do I keep re-runs from creating duplicates?
Make creation idempotent: creating a record that already exists should be a no-op or a skip, not a second copy. Combine that with a read-back check so re-runs recreate only what is genuinely absent. Together they mean a mistaken retry — from a false failure or a nervous operator — costs nothing instead of corrupting the store.
Does reading back after every write slow things down?
It adds a round trip per item, which matters at high volume and is trivial at the scale most integrations run. When throughput is a concern, batch the read-back — create a group, then query the store once for all their identifiers — so you keep the authoritative confirmation without paying a separate round trip for each record.