A free server can rehearse a handler. It cannot be the book that proves the handler already ran. That split is the whole field guide. Model quality can rise, drafts can get cleaner, and the receipt row still belongs on a store the team controls.
The failure looks small until money, mail, or a webhook fires twice. A process on a scratch host writes done into a local file, the host is reclaimed, and the next boot treats the work as new. The side effect was real. The memory of it was not.
A rehearsal stage can host the scene. It is a poor box office. This note is about outbound effects, not about a consumer offset. The offset problem is a reader advancing a cursor too early. The receipt problem is a sender forgetting that the outside world was already called.
What the scratch path is for
Free model access is useful for drafting the handler, the failure notes, and the tests. A free server is useful for running that draft against fixtures the operator can throw away. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those two options are availability claims supplied for this draft, not a quota, a hardware bill, or a promise that the path stays up.
No model name, token ceiling, or retention window is asserted here. None of those figures was verified against a primary source for this piece. A cleaner completion does not move the system of record. Software still has to remember what it already did after the process is gone.
The draft is a proposal. It is not an executed production trace. Treat generated code the way a team treats a patch from a new teammate: read it, run it where loss is cheap, and refuse to let it appoint itself as the ledger.
The invariant
Commit the receipt on the primary before the side effect, or do not perform the side effect. The primary is the database, queue, or object store the team already trusts for orders and audits. The scratch host may cache a copy for the length of a test. It may not be the only copy.
A receipt is a small row: an idempotency key, a status, a payload hash, and a timestamp the primary assigns. If the row is already done, the handler returns the stored result and does not call the downstream again. If the scratch disk disagrees with the primary, the primary wins.
That rule closes a class of double-send bugs that no smoother draft will fix by wording alone. Think of the free host as a practice room with a piano that can be rolled away overnight. The set list can be rehearsed there. The ticket stub that says a customer already paid has to live in the office safe.
A reproducible check
The following Python is an illustrative harness, not a benchmark and not a claim about any vendor runtime. It uses the standard library only so a reader can run it on a laptop. SQLite stands in for a primary the team owns. It is not a suggestion to keep the real ledger on the scratch host.
This harness is a proposal for a local check. It was not run against a vendor host while this note was written. The point is placement, not speed.
#!/usr/bin/env python3
"""Illustrative receipt placement. Not a production client."""
import hashlib
import json
import sqlite3
import tempfile
from pathlib import Path
def receipt_key(action: str, payload: dict) -> str:
body = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(f"{action}\n{body}".encode()).hexdigest()
def open_primary(path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS receipts (
key TEXT PRIMARY KEY,
status TEXT NOT NULL,
result TEXT
)
"""
)
conn.commit()
return conn
def claim(conn: sqlite3.Connection, key: str) -> str:
cur = conn.execute(
"INSERT INTO receipts(key, status, result) VALUES (?, 'claimed', NULL) "
"ON CONFLICT(key) DO NOTHING",
(key,),
)
conn.commit()
if cur.rowcount == 1:
return "owned"
row = conn.execute(
"SELECT status FROM receipts WHERE key = ?", (key,)
).fetchone()
return row[0] if row else "missing"
def finish(conn: sqlite3.Connection, key: str, result: str) -> None:
conn.execute(
"UPDATE receipts SET status = 'done', result = ? WHERE key = ?",
(result, key),
)
conn.commit()
def perform_once(conn: sqlite3.Connection, action: str, payload: dict, effect) -> str:
key = receipt_key(action, payload)
state = claim(conn, key)
if state == "done":
row = conn.execute(
"SELECT result FROM receipts WHERE key = ?", (key,)
).fetchone()
return row[0]
if state != "owned":
raise RuntimeError(f"refusing side effect; receipt state={state}")
result = effect(payload)
finish(conn, key, result)
return result
def demo() -> None:
calls = {"n": 0}
def send_mail(payload: dict) -> str:
calls["n"] += 1
return f"sent:{payload['to']}"
with tempfile.TemporaryDirectory() as tmp:
primary = open_primary(Path(tmp) / "primary.db")
payload = {"to": "ops@example.test", "body": "cutover check"}
first = perform_once(primary, "mail", payload, send_mail)
second = perform_once(primary, "mail", payload, send_mail)
assert first == second == "sent:ops@example.test"
assert calls["n"] == 1
print("primary_receipt_ok", calls["n"])
if __name__ == "__main__":
demo()
Save it as receipt_placement.py and run it where a lost temp directory is acceptable.
python3 receipt_placement.py
The expected line is primary_receipt_ok 1. A second call in the same process must not increment the effect. That is only the happy path. A leftover claimed row must refuse the send, which is what state != "owned" is for. A sweeper, not another prompt, decides whether that stuck claim is safe to finish or must be compensated.
Debugging a double send
When a drill sends twice, ignore the model's explanation and follow the disk. Print the receipt key, then query the primary, not the scratch log. If the primary has no row, the handler performed an effect without a claim.
If the primary has claimed and never done, the crash sat in the gap, and a sweeper owes a decision. If the primary has done and the effect still ran, the handler ignored its own row. Those three shapes have different fixes. Retrying the prompt is not one of them.
A placement probe makes the scratch host fail on purpose. Copy the fixture primary to a path the team keeps, delete the temporary directory that stood in for the free server, and open only the copied primary. Call the same payload again with a counter that starts at zero in the new process. The counter must stay at zero, and the stored result must come back.
# Fixture paths only. Do not point this at a production file.
cp /tmp/drill/primary.db "$HOME/receipt-fixtures/primary.db"
rm -rf /tmp/drill
sqlite3 "$HOME/receipt-fixtures/primary.db" \
"SELECT status, result FROM receipts LIMIT 5;"
If the only passing run is the one where scratch and primary share a fate, the test is grading the wrong disk. The command is a shape for the check, not a production runbook.
Red flags
The scratch host is holding the only receipt when a restart would resend. That is the first flag. The second is a generated handler that writes status to /tmp, or to an in-memory map, and then calls a real provider. The third is a review comment that calls the free server durable enough for now. Durability is a property of the store and the backup story, not a mood.
A fourth flag shows up in the prompt, not the disk. If the model is asked to decide whether a charge, a delete, or a customer email already happened, authority has moved into a draft. The model can propose the branch. The primary row decides the branch. A fluent account of a missing receipt is not a receipt.
Clock skew on a free server is another quiet flag. If expiry math lives only on that host's clock, a stepped clock can reopen a closed key. Store the deadline in the primary, and compare against a time the primary records. The scratch host may display the value. It should not invent it.
Better alternatives
Keep the receipt table, or an equivalent unique constraint, on the same primary that owns the business row. Use the free server as a client of that primary during a drill, with a test schema and fake downstreams. If the primary cannot be reached from the drill, stop the side effect. A skipped send is cheaper than a second send the ledger cannot remember.
When the team has no primary yet, the alternative is not to crown the free server. Stand up a store with backups and a named owner first, even if that store is a single small database on hardware the team administers. Free model access can still draft the migration and the handler. The draft waits until the store exists.
For pure read-only exploration, skip the receipt path entirely. Fixtures, recorded responses, and a local stub are enough. The moment the code can reach a real mailbox, payment API, or webhook target, the receipt rule turns on. A free server remains a fair place to run the stub. It is a bad place to be the only memory of a live call.
Exit criteria
Leave the free server as the status holder when any of these become true. The handler can cause a customer-visible side effect. The receipt must survive a host rebuild. More than one worker can claim the same key. An auditor asks where the proof lives, or a deploy pipeline would promote the scratch disk into the only copy of status.
Exit does not mean abandon the drafting loop. It means the loop's output is a patch against the primary schema, reviewed like any other patch. The free path keeps generating candidates. The primary keeps the bit that says the work finished.
A practical cut line sits in operations, not in taste. If losing the scratch host tonight would cause a resend, a double refund, or a gap in the audit trail, the receipt is already late. Move it before the next real call. Confidence in a draft is not a reason to stay.
Who should not use this split
Teams that need the free host to be their only database should not follow this guide as a workaround. They need a real store, not a clever prompt. Workloads that cannot tolerate a claim row stuck in claimed after a crash need a sweeper and a compensation path this sketch does not provide.
The harness also ignores authentication, encryption, and multi-region clocks. Copying it into a payment service as-is would be a mistake. Anyone who needs a published quota, a named model, or a retention guarantee should stop at the operator's current docs rather than infer them from a field note.
This article does not measure latency, does not rank providers, and does not say the free options are permanent. Readers who want a rehearsal bench can try the free model access and the free server for drafts and fixtures, and leave the receipt book on the primary.
Closing the ledger
The useful habit is boring, which is why it survives a trend cycle. Draft on the cheap path. Prove the effect once. Store the proof where a vanished scratch disk cannot erase it.
Better completions will keep arriving. The row that says the mail already went out should not have to arrive with them.
Top comments (0)