The failure never arrives at minute one. It arrives at minute ninety — after the loop has run twice, after the notebook says "working", and after you have already told someone it works.
That is the shape of most free-tier incidents in these notes. Not a dramatic outage, just a quiet divergence between what your script assumes about its environment and what the environment actually provides. An outbound call that is silently blocked. A directory that looked writable until the process restarted. A response body that parses in your editor and not in your harness.
Aircraft crews solved this class of problem decades ago with a checklist that takes less time than taxiing. The equivalent for an AI workflow is a preflight probe: a short script that asserts the environment's contract before you build an evening on top of it. It is boring, and it is the reason the evening survives.
I will not quote latency medians, token counts, or throughput figures here, because I did not measure them for this piece and a borrowed number is worse than no number. What follows is the probe I would run first, the routing table I keep beside it, and the exit codes that end the guessing.
The probe
Four checks, one real request, one measured number. The script exits non-zero the moment a contract is broken, so you fix the environment instead of debugging your own prompt.
#!/usr/bin/env bash
# preflight.sh — fail in ~40s instead of minute 90
set -uo pipefail
BASE_URL="${BASE_URL:?set BASE_URL to your model endpoint}"
KEY="${MODEL_API_KEY:?set MODEL_API_KEY}"
PROBE_DIR="${PROBE_DIR:-$HOME/.preflight}"
fail() { printf 'FAIL %s\n' "$1" >&2; exit "$2"; }
# 1. runtime floor
command -v python3 >/dev/null || fail "python3 missing" 2
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' \
|| fail "python3 >= 3.10 required" 2
# 2. writable scratch space that survives a re-run
mkdir -p "$PROBE_DIR" && touch "$PROBE_DIR/.w" && rm "$PROBE_DIR/.w" \
|| fail "probe dir not writable: $PROBE_DIR" 2
# 3. outbound HTTPS (blocked egress is the classic free-tier surprise)
curl -sS -m 8 -o /dev/null -w '%{http_code}' https://example.com \
| grep -q '^200$' || fail "no outbound HTTPS" 2
# 4. one real request, with a shape assertion and a measured duration
start=$(date +%s%N)
body=$(curl -sS -m 60 -X POST "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"reply with the single word: ok"}],"max_tokens":8}')
rc=$?
end=$(date +%s%N)
[ $rc -eq 0 ] || fail "transport error (curl $rc)" 3
printf '%s\n' "$body" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["choices"][0]["message"]["content"]' \
|| fail "response shape mismatch" 3
printf 'OK probe_ms=%d\n' $(( (end - start) / 1000000 ))
The nanosecond timestamps matter more than they look. One measured round trip tells you whether the endpoint is warm on this host today; a remembered number from last week tells you nothing. Note what the script does not do: it does not retry. A preflight that retries hides exactly the flakiness it exists to expose.
Run it before you write product code:
BASE_URL="$BASE_URL" MODEL_API_KEY="$KEY" ./preflight.sh; echo "exit=$?"
Reading the exit codes
Three outcomes, three different afternoons.
exit=2 is an environment problem and has nothing to do with the model. Missing runtime, read-only volume, blocked egress. Fix the box. exit=3 means the transport worked and the payload did not — wrong path, expired key, an error envelope where you expected a completion. Dump the raw body before you touch your parser:
curl -sS -D headers.txt -X POST "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"ok"}],"max_tokens":8}' \
| tee body.json | head -c 400
Nine times out of ten body.json contains an error object and headers.txt contains the status. Your prompt is innocent. exit=0 means the contract holds and every later failure is your code, which is a much better place to start debugging from.
The routing table
Free model access and a free server option change the economics of experiments, not the reliability requirements of products. I keep a two-column rule: a single failure may cost seconds, or it may cost a user. Only the first column belongs on a free tier.
| Workload | Preflight-then-free | Earliest signal it will bite |
|---|---|---|
| Prompt and schema iteration | Yes | Shape mismatch at exit=3
|
| Low-volume batch classification | Yes, with a stop condition | Wall-clock drift across runs |
| Long-running webhook receiver | No | Process gone after restart |
| User-facing call with a latency budget | No | Probe timing already near budget |
| Anything holding regulated data | No | You cannot audit egress |
The table is deliberately small because the decision is small. If the artifact of a failed run is a log line you delete, use the free option and probe it first. If the artifact is a customer email, run the probe anyway and then route the call somewhere with a written contract.
What the probe cannot see
It cannot see quota definitions that change between weeks, and it cannot see concurrency ceilings until you push past one. It cannot see model version drift, which is the quiet one: the endpoint answers, the shape matches, the answers shift. It also cannot promise that scratch space survives a redeploy — a writable directory today says nothing about tomorrow.
So treat the probe as a smoke test, not a service-level agreement, and re-run it at the start of every session rather than once at the start of the project. When a free-tier page states a quota, read the page on the day you depend on it; blog posts, including this one, go stale faster than docs do.
Who should not use this
Anyone with a regulatory obligation to show where data went. Anyone who has promised sub-second latency. Anyone whose job queue must survive a restart without an external store. And anyone who reads probe_ms as a benchmark — it is one request, on one host, at one moment, and it is meant to be boring.
If you want to run the same script before your next experiment, point BASE_URL and MODEL_API_KEY at MonkeyCode's free model access and its free server option and watch which exit code you get first. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Forty seconds of preflight is cheaper than an evening of confidence. That is the whole method, and it is the part I would repeat.
Top comments (0)