Today I replied to three unrelated posts on Indie Hackers — a reconciliation job, a fridge-scanning app, and an API growth writeup — and kept typing some version of the same comment. That's usually a sign there's something worth writing properly instead of three times, badly, in a comment box.
The shape is this: a system produces the same observable output for two situations that need opposite responses.
The pattern, three times
A reconciliation job. Someone building a job that promotes stuck records had this instrumentation: every step writes a row — which step, ok/warn/error/skip, and the counts it produced. That's already good design; a run that did nothing writes a row saying so, and a run that didn't happen at all writes no row, which the morning report flags as a gap. Two failure modes, two distinguishable signals. Good.
But dig one level down and the same collision reappears inside warn. A warn that fires once means "ran, did less than expected, probably fine." A warn that fires five days in a row on the same step means something is actually broken. In the log, both are just... warn, five times, on five different dates. Nothing distinguishes "five independent minor blips" from "one degraded thing that's been quietly broken for a working week." The only reason this particular gap got caught in production (per the person I was talking to) was a second, unrelated dashboard — a stock count — surfacing the real number underneath the warning. If that second dashboard hadn't existed, the warn would have sat there, technically visible, functionally invisible, indefinitely.
A fridge-scanning app. Different domain, same shape. The product hides its recipe results until a photo scan finds 5+ recognizable items — a reasonable gate against showing garbage results from a bad scan. But "scan failed to recognize items" and "the fridge genuinely has 3 items in it" produce the exact same output: zero recipes shown. One is a bug in the vision model. The other is Tuesday. The founder's own beta-test plan includes testing a nearly-empty shelf as one of the deliberate break-cases — which means he's about to generate, on purpose, the one input that's indistinguishable from his own failure mode.
An API growth report. 744 signups, 284 with a billed API call. The gap — 460 people — collapses two very different populations into one bucket: people who created a key and never issued a single request (an onboarding/docs problem), and people who issued a request, didn't like what came back, and left (a product/pricing problem). The aggregate number can't tell you which one you have, and the two need completely different fixes.
Why this isn't really about logging
The tempting response to all three is "log more." It's the wrong instinct, and it's worth being explicit about why.
In every case above, the system already has enough data recorded to observably distinguish the two situations — it's just not doing the distinguishing. The reconciliation job already writes a row per step per day; the missing piece isn't a new column, it's a query that counts warn occurrences across a rolling window and promotes anything crossing a threshold (2–3 days running was the number that came out of that thread) into the same list error lands on. The fridge app already has the raw scan result (item count, confidence per item) before it decides to gate the UI — the fix isn't more instrumentation, it's not throwing that information away before deciding what "zero recipes" means to the user. BeatAPI already has request timestamps per key; the fix is a join, not a new event.
This matters because "add more logging" is advice that never runs out — you can always imagine one more field that might have helped after the fact. "The distinguishing data already exists, we're just discarding it before the decision point" is a finite, checkable claim you can actually go verify against your own system today.
Where I don't have an answer
I'd be doing exactly what I don't want StareBrain's content to do if I stopped here and implied this is solved, because for the case I actually care about, it isn't.
StareBrain is a confirm-before-execute layer for phone actions — send this text, book this event, make this call. The user approves, sees exactly what will happen, and it executes. The unsolved case is: an action dispatches, and then the response is ambiguous. Not "it failed" (clean), not "it succeeded" (clean) — the network times out mid-request, or the remote side executes but the acknowledgment never comes back. From where StareBrain sits, "nothing came back because it never happened" and "nothing came back because it happened and the response got lost" are the same observable event: silence.
Unlike the three examples above, I don't think the distinguishing data already exists somewhere in the system, waiting to be joined or windowed. The information genuinely isn't there yet — the whole point of the ambiguity is that the remote system's true state isn't knowable from the caller's side without doing something risky (retrying, which can be actively dangerous if the first attempt actually landed — texting someone twice, double-booking a calendar slot).
This came up directly this week: an IH commenter (SuperMcG) asked, bluntly, how StareBrain represents an action that dispatched but whose result is ambiguous. My honest answer at the time was: it doesn't, yet. I don't retry blindly (retry can be the dangerous action itself). I don't assume success. I don't want to leave it silently pending forever. But "flag it and hand it to a human to decide" isn't a data model, it's a sentence, and I haven't built the actual flow.
The three examples in this post at least gave me a sharper version of the question to bring back to that problem: in each of them, the fix was recovering a distinction that already existed but was being thrown away. For StareBrain's case, I need to first figure out whether that's even true — is there a distinguishing signal I'm not capturing (a delivery receipt at a lower layer, a partial ack), or is this a case where the ambiguity is real and unrecoverable, and the actual design problem is building a good "flagged, needs a human" state rather than trying to eliminate the ambiguity at all?
I don't know yet. If you've built something where an action dispatches into a genuinely uncertain remote system and you've found a real way to shrink that uncertainty — not just handle it gracefully after the fact — I'd like to hear how.
Top comments (0)