DEV Community

Taylor Zhu
Taylor Zhu

Posted on

The Free Server Is the Fragile Part: A Runtime Readiness Checklist for Hosted AI Prototypes

Your free-tier prototype does not fail at the model call. It fails at 3 a.m., when the instance has been recycled, the write you thought was durable is gone, and nobody wrote down what "working" was supposed to mean.

So write it down. Below is a runtime-readiness checklist you can copy into a repo: explicit claims, one evidence artifact per claim, and a fail-closed rule for every row. Missing evidence is never a pass.

This is the server-side companion to model-allowance checks. The model budget is easy to reason about. The runtime contract is where assumptions quietly rot.

Four assumptions that break first on a free server

Free hosting is not a smaller version of paid hosting. It is a different failure contract. Watch these four in particular:

  • Durability. Local disk may be ephemeral. Anything you did not write to an external store can vanish on the next cold start.
  • Warmth. Instances sleep or get recycled. Your p50 latency is meaningless if the first request after idle takes seconds.
  • Address stability. Hostnames, ports, and headers can change between deploys or between regions.
  • Egress. Outbound calls to a model endpoint may be rate-limited or blocked from that network path, independently of your own quota.

None of these are bugs. They are the terms. Your job is to record which ones your app actually depends on.

The checklist: claim, evidence, fail-closed rule

Every row has three parts. A claim without evidence is BLOCKED, not PASS. That single rule removes most of the self-deception in readiness reviews.

# Claim Evidence artifact Fail-closed rule
R1 First request after idle completes cold-start.json with timestamp + ms No sample in last 7 days → BLOCKED
R2 App survives instance recycle without data loss state-roundtrip.json before/after restart Read-back mismatch → BLOCKED
R3 Write path is idempotent dup-write.json with two identical calls Duplicate row or 5xx → BLOCKED
R4 Max accepted payload is known payload-limit.json with the failing size Limit unknown → BLOCKED
R5 Outbound model call works from this network egress.json status + latency Any non-2xx in last run → BLOCKED
R6 Response headers leak nothing internal headers.txt Internal host/IP/version present → BLOCKED
R7 Rollback target still deploys rollback.log Untested in last 30 days → BLOCKED

Notice R7. A free server makes rollback more important, not less, because you cannot assume the previous instance is still warm or still addressable.

The probe: one script, seven artifacts

Here is a stdlib-only Python probe. It writes one JSON file per gate so each row of the table has a named artifact. Copy it, set two env vars, run it. Nothing here is MonkeyCode-specific; it talks to whatever URL you point it at.

# probe_runtime.py -- stdlib only
import json, os, sys, time, urllib.request, urllib.error

BASE = os.environ["TARGET"].rstrip("/")
TOKEN = os.environ.get("PROBE_TOKEN", "")
OUT = os.environ.get("EVIDENCE_DIR", "evidence")
os.makedirs(OUT, exist_ok=True)

def call(path, method="GET", body=None, timeout=60):
    data = json.dumps(body).encode() if body is not None else None
    r = urllib.request.Request(BASE + path, data=data, method=method)
    r.add_header("content-type", "application/json")
    if TOKEN:
        r.add_header("authorization", "Bearer " + TOKEN)
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(r, timeout=timeout) as resp:
            return resp.status, resp.read(), dict(resp.headers), (time.perf_counter()-t0)*1000
    except urllib.error.HTTPError as e:
        return e.code, e.read(), dict(e.headers), (time.perf_counter()-t0)*1000
    except Exception as e:
        return 0, str(e).encode(), {}, (time.perf_counter()-t0)*1000

def save(name, obj):
    obj["recorded_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    with open(os.path.join(OUT, name), "w") as f:
        json.dump(obj, f, indent=2)
    print(name, obj.get("result", obj.get("status")))

# R1: cold start (run this as the first request after an idle window)
code, _, _, ms = call("/health")
save("cold-start.json", {"status": code, "ms": round(ms, 1)})

# R3: duplicate write, same key twice
key = "probe-" + str(int(time.time()))
for i in (1, 2):
    code, _, _, _ = call("/state", "POST", {"id": key, "value": "v%d" % i})
code_r, body, _, _ = call("/state/" + key)
save("dup-write.json", {"status": code_r, "body": body[:200].decode("utf-8", "replace")})

# R4: document the payload ceiling instead of guessing it
for size in (1024, 65536, 1048576, 4194304):
    code, _, _, _ = call("/state", "POST", {"id": key + "-sz", "blob": "x" * size})
    if code >= 400 or code == 0:
        save("payload-limit.json", {"first_failing_bytes": size, "status": code})
        break
else:
    save("payload-limit.json", {"first_failing_bytes": None, "status": "no-limit-hit"})

# R5: outbound egress to the model endpoint
code, body, _, ms = call("/generate", "POST", {"prompt": "reply with the word ok"})
save("egress.json", {"status": code, "ms": round(ms, 1), "body": body[:200].decode("utf-8", "replace")})

# R6: header hygiene
_, _, headers, _ = call("/health")
with open(os.path.join(OUT, "headers.txt"), "w") as f:
    f.write("\n".join("%s: %s" % kv for kv in sorted(headers.items())))
print("headers.txt")
Enter fullscreen mode Exit fullscreen mode

R2 needs a restart, so it is a two-step manual gate on purpose. Write a value, recycle the instance from the dashboard, then read it back and store the before/after pair. Automating that would hide the exact moment you are trying to observe.

Turning artifacts into a verdict

Do not let a human eyeball this. Twenty lines of code make the fail-closed rule mechanical:

# verdict.py -- PASS only with fresh, complete evidence
import json, os, time
REQUIRED = ["cold-start.json", "state-roundtrip.json", "dup-write.json",
            "payload-limit.json", "egress.json", "headers.txt", "rollback.log"]
TTL_DAYS = {"cold-start.json": 7, "rollback.log": 30}

blocked = []
for name in REQUIRED:
    path = os.path.join("evidence", name)
    if not os.path.exists(path):
        blocked.append((name, "missing"))
        continue
    ttl = TTL_DAYS.get(name)
    age_days = (time.time() - os.path.getmtime(path)) / 86400
    if ttl and age_days > ttl:
        blocked.append((name, "stale: %.1f days" % age_days))

for name, why in blocked:
    print("BLOCKED", name, "-", why)
print("VERDICT:", "PASS" if not blocked else "BLOCKED")
raise SystemExit(0 if not blocked else 1)
Enter fullscreen mode Exit fullscreen mode

The exit code matters more than the printout. Wire it into CI as a required check so a deploy cannot skip the gate by forgetting to run it.

Where the two free options fit

A hosted prototype on free model access plus a free server is a genuinely cheap way to run this drill, because both halves of the contract are in play at once: the model path (R5) and the runtime path (R1, R2, R4).

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Because I cannot verify your deployment from here, treat the free-tier specifics — current token allowance, instance sleep behavior, disk durability, and any hardware details — as things you must read off the project's own current documentation and confirm with the probe. Limits on free offerings change; a number quoted in a blog post is not evidence. That is precisely why R1 and R2 exist as gates rather than footnotes.

A concrete sequence that works:

  1. Deploy a three-endpoint demo (/health, /state, /generate) to the free server option.
  2. Run probe_runtime.py immediately after deploy, then again after an idle window you did not control.
  3. Recycle the instance from the dashboard and complete the R2 read-back by hand.
  4. Point /generate at the free model access endpoint and log the status you actually got.
  5. Run verdict.py. If it exits non-zero, you have a checklist result, not a launch.

Step 2 is the one people skip. A probe run right after a warm deploy measures almost nothing.

What this checklist will not tell you

Be honest about the boundaries:

  • It does not measure throughput. Seven gates say nothing about concurrency limits under real traffic.
  • It does not prove durability. One read-back shows the state survived one recycle. It is evidence, not a guarantee.
  • It ages. Hence the TTLs. A green verdict from three weeks ago is a historical document.
  • It is not a security review. R6 only checks header hygiene, not auth, isolation, or secrets handling.

Who should not use this approach: teams with a hard sub-second p95 SLA, regulated data-residency requirements, or write paths that must be strictly durable. For those, run the checklist anyway — but expect it to correctly block you, and budget for infrastructure that matches the contract.

Start with the boring gate

If you take one thing away: the row you are most tempted to mark "obviously fine" is the row that will page you. Write the claim, name the artifact, define the fail-closed rule, and let an exit code say no for you. Then the next person who inherits the prototype can read seven files instead of guessing what you knew.

Top comments (0)