A health check can lie to you politely. The process is alive. The port is bound. The endpoint still returns garbage.
So what does "up" actually mean? I don't treat a running PID as evidence of anything. I treat a probe with a clock as evidence.
This article busts five uptime myths I keep hearing around free-tier instances and free model endpoints. Then it hands you a probe you can run today. The probe answers one boring question honestly: did this thing restart, and did anyone notice?
Myth 1: "It's running, so it's up."
There are three separate states here. People collapse them into one.
- Process exists.
kill -0 $PIDsucceeds. - Socket is bound.
ss -ltnp | grep 8000shows a listener. - Handler works.
curl -fsS http://127.0.0.1:8000/healthreturns a valid body.
A crashed worker with a live supervisor passes check 1. A hung event loop passes check 2. Only check 3 touches the code path your users touch.
Corrected model: "Up" is not a state. It is a verdict about the last N probes inside a tolerance window. If the newest probe is older than the window, the answer is "unknown", not "fine".
That word matters. Most dashboards I've seen render "unknown" as green.
Myth 2: "Free instances stay warm between requests."
Maybe. Maybe not. You cannot verify warmth by feel, and you should not build on it.
The cheap test is identity. Ask the process who it is:
- A
boot_idgenerated at import time. - A monotonic start timestamp.
- The PID, for cross-checking with host logs.
If boot_id changes, you got a new process. If uptime_s is smaller than the gap since your last probe, something reset. No guessing required.
Corrected model: assume every request can hit a cold process. Move heavy initialization out of the request path or make it lazy and measurable. Then log boot_id on every response, so restart counting becomes a GROUP BY, not an argument.
Myth 3: "My in-memory cache is a cache."
A cache you cannot rebuild is storage wearing a cache costume.
Restart the process and watch what happens:
curl -s localhost:8000/health | jq '.in_memory_hits' # 27 # restart the process here curl -s localhost:8000/health | jq '.in_memory_hits' # 1 If that number must survive, it was never a cache. It was durable state, and you put it in the most fragile tier you own.
Corrected model: every in-memory structure gets one of two labels. Rebuildable (losing it costs latency) or durable (losing it costs correctness). The second label means it lives off the instance, full stop.
Myth 4: "Logs on local disk are an audit trail."
Local disk on a rented instance is a scratchpad. It is not an archive, and it is not your incident record.
When the instance is replaced, the scratchpad goes with it. The logs you need most are exactly the ones written in the last minute before the replacement.
Corrected model: write locally for speed, ship off-box for truth. If you cannot ship, accept that your audit trail has holes and say so in the runbook instead of pretending otherwise.
Myth 5: "The model endpoint is a stable dependency."
This is where MonkeyCode's free model access shows up in my workflow. I use it as a network dependency with unknown latency, not as a magic constant.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Three rules I apply to any model call, free or paid:
- Always set a timeout. A model call without a timeout is an outage waiting for load.
- Cache by prompt hash on disk, with a TTL. Not in RAM. See Myth 3.
- On HTTP 429, stop. Do not retry around a rate limit. Back off, degrade, or return a clear error. Retry storms are self-inflicted.
"Free" changes the price. It does not change the physics of a remote call.
The artifact: a restart probe that runs off-box
The key design choice: the poller must not live on the box it watches. A process cannot report its own death.
Server side: tell the truth about restarts
# app.py — reference implementation, not a benchmark import os, time, uuid from fastapi import FastAPI BOOT_ID = uuid.uuid4().hex BOOT_TS = time.time() STATE = {"hits": 0} app = FastAPI() @app.get("/health") def health(): STATE["hits"] += 1 return { "boot_id": BOOT_ID, "uptime_s": round(time.time() - BOOT_TS, 3), "pid": os.getpid(), "in_memory_hits": STATE["hits"], } Run it with uvicorn app:app --host 0.0.0.0 --port 8000.
Client side: record gaps and identity changes
# probe.py — run this on a different machine import csv, datetime as dt, time, httpx URL = "http://TARGET:8000/health" INTERVAL = 30 GAP_LIMIT = 90 prev_boot, last_ok = None, None with open("probe.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["ts", "ok", "boot_id", "uptime_s", "gap_s", "note"]) while True: t0 = time.monotonic() row = [dt.datetime.utcnow().isoformat(timespec="seconds"), 0, "", "", "", ""] try: body = httpx.get(URL, timeout=5.0).json() gap = "" if last_ok is None else round(t0 - last_ok, 1) notes = [] if prev_boot and body["boot_id"] != prev_boot: notes.append("restart") if isinstance(gap, float) and gap > GAP_LIMIT: notes.append(f"gap>{GAP_LIMIT}s") if body["uptime_s"] < gap if isinstance(gap, float) else False: notes.append("clock-mismatch") row = [row[0], 1, body["boot_id"], body["uptime_s"], gap, ",".join(notes)] prev_boot, last_ok = body["boot_id"], t0 except Exception as e: row[5] = type(e).__name__ w.writerow(row) f.flush() time.sleep(max(0, INTERVAL - (time.monotonic() - t0))) The clock-mismatch note is the useful one. It means the reported uptime is shorter than the wall time since your last successful probe. Either the process restarted between probes, or two clocks disagree. Both are worth knowing.
Summarize it
import csv rows = list(csv.DictReader(open("probe.csv"))) fails = [r for r in rows if r["ok"] != "1"] restarts = [r for r in rows if "restart" in r["note"]] gaps = [r for r in rows if "gap>" in r["note"]] print(f"{len(rows)} samples | {len(fails)} failures | {len(restarts)} restarts | {len(gaps)} gaps") Run it for 24 hours before you trust any instance with real traffic. One day of CSV beats six months of assumptions.
Decision table: where does each piece of state live?
| State | Safe in RAM | Safe on instance disk | Needs off-box |
|---|---|---|---|
| Response cache | Yes, if rebuildable | Yes, with TTL | Only if rebuild is expensive |
| Request counters | No | No | Yes |
| Background job queue | No | No | Yes |
| Debug logs | No | No | Yes |
| User data | No | No | Yes |
| Compiled templates | Yes | Yes | No |
Read the table as a threat model, not a rulebook. Every "Yes" assumes a restart is survivable.
Limitations, and who should skip this
Be honest about the probe's blind spots:
- It measures reachability from one vantage point. A regional network split looks like downtime.
- It cannot see partial failures. A handler returning stale 200s looks healthy.
- A 30-second interval misses short restarts entirely. Tighten it and you pay in traffic.
- It tells you that something restarted, never why. Pair it with off-box logs.
Do not use this approach if you need contractual uptime guarantees, regulated-data retention, or per-write durability. Free-tier instances are for experiments, demos, and small internal tools. If the data loss would cost money, buy the boring database.
And if you already run proper monitoring, you don't need my CSV. You need better alert routing, which is a different article.
What changes on Monday
Stop asking "is it running?" Start asking "when did I last prove it works, from somewhere else?"
If you want to test the probe against MonkeyCode's free server option and free model access, run it for a day first. Let the CSV argue with your assumptions.
What's the oldest green dashboard tile you've ever trusted? Go check its probe history.
