DEV Community

Harper Zhu
Harper Zhu

Posted on

Give Your Spike an Exit Code, Not a Retrospective

A spike ends when the timer does, but the decision tends to end somewhere else entirely. Three engineers read the same notes after a ninety-minute box closes, and each of them walks away with a different verdict: one calls the probe a success, another keeps pointing at a timeout that never fully disappeared. The probe was probably fine; what was missing was a stop rule that could be executed instead of argued. Notes describe a spike, but they cannot decide one, and that gap is where most of the post-spike hour is spent.

The fix is unglamorous and small. Write the thresholds before the first probe runs, append every probe to a line-delimited ledger, and let a short script read both files and print a verdict with an exit code. This is a proposed workflow with illustrative code, not a report of a specific team's results, so treat the numbers below as placeholders you replace with your own. The point is that the last ten minutes of the spike become a command rather than a conversation.

Freeze the hypothesis before the clock starts

A decision rule has to exist before the evidence arrives, otherwise the evidence quietly rewrites the rule. The file below registers one hypothesis, the smallest number of probes that count as a sample, and the two thresholds that bound the outcome. Anything not written here cannot influence the verdict later, which is the whole reason to keep it this short.

{
  "spike": "cache-write-fence",
  "deadline_minutes": 90,
  "min_probes": 8,
  "ship_if": { "rate": 0.9 },
  "kill_if": { "failures_with_same_signature": 3 },
  "signature_fields": ["error_class", "stage"],
  "hypothesis": "the write fence prevents partial writes under concurrent retries"
}
Enter fullscreen mode Exit fullscreen mode

The ship_if rule asks whether the mechanism works at all, while kill_if asks whether it fails in the same place three times. That second rule matters more than it looks, because repeated identical failures in a ninety-minute box are usually structural rather than incidental. Freezing the file with sha256sum spike/hypothesis.json > spike/hypothesis.sha256 gives you a cheap way to prove later that nobody tuned the thresholds to fit the results.

Keep a ledger of probes, not a diary

Prose notes mix observations with interpretation, and the checker needs only the former. A ledger keeps one JSON object per line, which means a probe can be appended by a human, a wrapper script, or a test runner without any coordination between them. The note kind records context such as an environment change, and it stays out of the pass rate because the checker filters on kind.

{"t":"2026-09-15T09:02:11Z","kind":"probe","id":"p1","ok":true,"ms":421,"error_class":null,"stage":"write"}
{"t":"2026-09-15T09:02:39Z","kind":"probe","id":"p2","ok":false,"ms":903,"error_class":"TimeoutError","stage":"fsync"}
{"t":"2026-09-15T09:04:02Z","kind":"note","text":"raised probe timeout to 1500ms; /tmp is on overlayfs"}
{"t":"2026-09-15T09:05:20Z","kind":"probe","id":"p3","ok":false,"ms":1502,"error_class":"TimeoutError","stage":"fsync"}
Enter fullscreen mode Exit fullscreen mode

Two fields carry unusual weight here. The stage value is what turns three unrelated timeouts into one repeated signature, and ms is what lets a reader tell a real failure from a probe that was simply given too little time. Honest classification of error_class is the weakest link in the whole format, because a wrapper that labels everything nonzero_exit produces a signature that can never repeat.

Let a checker say SHIP, KILL, or STILL-OPEN

The script below prints one line and exits with a code that a shell or CI job can act on, which is the part a retrospective cannot offer. Exit codes are cheap to wire into habits: 0 means ship, 1 means kill, and 2 means the spike has not earned either verdict yet.

#!/usr/bin/env python3
"""spike_verdict.py - print SHIP, KILL, or STILL-OPEN from a pre-registered ledger."""
import json
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path


def load_events(path):
    lines = Path(path).read_text().splitlines()
    return [json.loads(line) for line in lines if line.strip()]


def parse(ts):
    return datetime.fromisoformat(ts.replace("Z", "+00:00"))


def decide(hyp, events, now=None):
    now = now or datetime.now(timezone.utc)
    probes = [e for e in events if e.get("kind") == "probe"]
    start = min((parse(e["t"]) for e in events), default=now)
    minutes = (now - start).total_seconds() / 60
    passed = sum(1 for p in probes if p.get("ok"))
    rate = passed / len(probes) if probes else 0.0
    sigs = Counter(
        tuple(p.get(f) for f in hyp["signature_fields"])
        for p in probes if not p.get("ok")
    )
    signature, repeats = sigs.most_common(1)[0] if sigs else (None, 0)
    if len(probes) >= hyp["min_probes"] and rate >= hyp["ship_if"]["rate"]:
        return "SHIP", f"{passed}/{len(probes)} probes passed ({rate:.0%})"
    if repeats >= hyp["kill_if"]["failures_with_same_signature"]:
        return "KILL", f"{repeats} failures share signature {signature}"
    if minutes >= hyp["deadline_minutes"]:
        return "KILL", f"clock expired at {minutes:.1f} min, {passed}/{len(probes)} passed"
    return "STILL-OPEN", f"{passed}/{len(probes)} passed, {minutes:.1f} min used"


if __name__ == "__main__":
    hypothesis = json.loads(Path(sys.argv[1]).read_text())
    code, detail = decide(hypothesis, load_events(sys.argv[2]))
    print(f"{code}: {detail}")
    sys.exit({"SHIP": 0, "KILL": 1, "STILL-OPEN": 2}[code])
Enter fullscreen mode Exit fullscreen mode

The order of the rules is a deliberate choice worth stating out loud. Evidence is evaluated before the clock, so a spike that reaches its ship threshold in minute eighty-four still ships, and an expired clock only kills a spike that never earned a verdict. A run against the sample ledger looks unremarkable, which is exactly the property you want from a decision tool.

$ python spike_verdict.py spike/hypothesis.json spike/events.jsonl
STILL-OPEN: 3/4 passed, 6.4 min used
$ echo $?
2
Enter fullscreen mode Exit fullscreen mode

Record the clock honestly, including setup

A ninety-minute box that starts at the first successful probe is not a ninety-minute box, so the wrapper below opens the ledger at the moment the command starts and measures it with a hard timeout. Each invocation appends one probe line, which keeps manual bookkeeping out of the loop and makes the ledger append-only by construction.

#!/usr/bin/env bash
# usage: LEDGER=spike/events.jsonl PROBE_STAGE=fsync spike-run p3 -- ./probe.sh
set -u
LEDGER="${LEDGER:-spike/events.jsonl}"
ID="$1"; shift; [ "${1:-}" = "--" ] && shift
START=$(date -u +%s%3N)
OUT=$(mktemp)
timeout "${PROBE_TIMEOUT:-20s}" "$@" >"$OUT" 2>&1; RC=$?
END=$(date -u +%s%3N)
OK=$([ "$RC" -eq 0 ] && echo true || echo false)
ERR=$([ "$RC" -eq 124 ] && echo TimeoutError || echo nonzero_exit)
printf '{"t":"%s","kind":"probe","id":"%s","ok":%s,"ms":%s,"error_class":%s,"stage":"%s"}\n' \
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$ID" "$OK" "$((END-START))" \
  "$([ "$OK" = true ] && echo null || printf '"%s"' "$ERR")" "${PROBE_STAGE:-unknown}" >>"$LEDGER"
cat "$OUT"; exit "$RC"
Enter fullscreen mode Exit fullscreen mode

Everything before that first probe is unpaid clock, and it is usually paid to provisioning: an endpoint, a credential, a machine that can hold the workload. The MonkeyCode project is one option that removes part of that tax, since it advertises free model access plus a free server option, with a free allowance described as ten million tokens. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those availability claims come from the project rather than from independent verification here, so confirm the current limits yourself before you design a spike around them; the wrapper does not care which endpoint answers, only that one answers inside the box.

What this workflow cannot do

A checker certifies that a rule was applied, not that the probe measured something meaningful, and that distinction limits where the pattern belongs. Latency and throughput questions need distributions rather than pass rates, since eight probes cannot separate an eighty-five percent mechanism from a ninety-five percent one. The wrapper's signature quality depends entirely on honest error_class values, and date +%s%3N is GNU-specific, so macOS users need gdate or a small Python replacement. Anyone whose spike is a single long run rather than repeated probes should skip the ledger and keep the hypothesis file, and teams that cannot freeze thresholds before the first probe will only generate confident nonsense.

A killed spike is still an asset when its ledger survives, because the next hypothesis can be diffed against the signatures that already failed. That is the quiet payoff of writing the stop rule first: the meeting disappears, the negative evidence stays searchable, and the next ninety minutes start from a fact instead of a memory. If repeated spikes are part of your week, the ledger plus the checker is smaller than the retrospective it replaces, and the free tier in the MonkeyCode repository is worth a look the next time provisioning eats the first ten minutes of your box.

Top comments (0)