The Free Server Slept Overnight: Four State Claims Your App Can't Back Up
Your app worked at 6pm. At 9am it half-worked. Nothing changed in git. So what changed? The process died and came back. That is the whole story. Most "free server" bugs are not code bugs. They are state bugs wearing a
Your app worked at 6pm. At 9am it half-worked.
Nothing changed in git. So what changed?
The process died and came back. That is the whole story.
Most "free server" bugs are not code bugs. They are state bugs wearing a costume.
This post is not about portability. Portability asks "does it run there?" Durability asks "does it still run tomorrow?" Different question, different probe.
Below is a small app, a restart probe, and four claims I keep hearing. Each claim gets a test you can run in about ten minutes.
What this post does and does not claim
I am not claiming your free server is unreliable. Many are fine.
I am claiming you have not measured which state survives a restart. That gap is where the surprises live.
MonkeyCode advertises free model access and a free server option. Those are operator-supplied availability claims, not guarantees I verified. Treat them as a place to run the probe, not as proof of anything.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The test app: five state surfaces, one file
Keep it boring. Dependencies stay minimal.
# app.py (FastAPI; pip install fastapi uvicorn)
import os, uuid
from pathlib import Path
from fastapi import FastAPI, Response
from typing import Optional
app = FastAPI()
BOOT_ID = uuid.uuid4().hex
DATA = Path(os.environ.get("DATA_DIR", "./data"))
DATA.mkdir(parents=True, exist_ok=True)
DISK_COUNTER = DATA / "counter.txt"
TMP_MARKER = Path("/tmp/restart_probe_marker")
MEM = {"n": 0}
@app.get("/bump")
def bump(response: Response, sid: Optional[str] = Cookie(default=None)):
MEM["n"] += 1
disk = int(DISK_COUNTER.read_text()) if DISK_COUNTER.exists() else 0
DISK_COUNTER.write_text(str(disk + 1))
TMP_MARKER.write_text(BOOT_ID)
sid = sid or "sess-" + uuid.uuid4().hex[:8]
response.set_cookie("sid", sid, httponly=True)
return {"boot_id": BOOT_ID, "mem": MEM["n"], "sid": sid}
@app.get("/state")
def state(sid: Optional[str] = Cookie(default=None)):
return {
"boot_id": BOOT_ID,
"mem": MEM["n"],
"disk": int(DISK_COUNTER.read_text()) if DISK_COUNTER.exists() else None,
"tmp_marker": TMP_MARKER.exists(),
"sid": sid,
"env_probe": os.environ.get("PROBE_TOKEN"),
}
Five surfaces: process memory, the data directory, /tmp, a client cookie, and an env var.
That covers most of what people assume is "persisted."
The probe: snapshot, restart, snapshot
Run it against local Docker first. Then point it at the free server.
# probe.py (python probe.py http://127.0.0.1:8000 "docker compose restart app")
import json, subprocess, sys, time, urllib.request, http.cookiejar
BASE = sys.argv[1]
RESTART_CMD = sys.argv[2]
jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
def call(path):
with opener.open(BASE + path, timeout=30) as r:
return json.loads(r.read())
before = call("/bump")
state_before = call("/state")
print("restart:", RESTART_CMD)
subprocess.run(RESTART_CMD, shell=True, check=True)
for _ in range(30):
try:
state_after = call("/state")
break
except Exception:
time.sleep(2)
else:
sys.exit("server never came back")
print(json.dumps({"before": state_before, "after": state_after}, indent=2))
rows = [
("boot_id changed (control)", state_before["boot_id"] != state_after["boot_id"]),
("mem counter kept", state_after["mem"] >= state_before["mem"]),
("data dir kept", state_after["disk"] == state_before["disk"]),
("/tmp marker kept", state_after["tmp_marker"] is True),
("cookie kept", state_after["sid"] == state_before["sid"]),
("env var kept", state_after["env_probe"] == state_before["env_probe"]),
]
for name, ok in rows:
print(("PASS " if ok else "DRIFT " ) + name)
Read the first line before anything else. If boot_id did not change, you did not restart anything.
A probe against a process that never died proves nothing. This is the control, not a nice-to-have.
Claim 1: "If I write a file, it stays."
It stays until the container is replaced. Replacement is not the same as restart.
Some platforms restart the process but keep the filesystem. Others rebuild the unit from the image.
Your counter.txt tells you which one you got.
Expected result: DRIFT on /tmp, and DRIFT on ./data unless you mounted a volume.
Corrected model: decide where durability is required, then test that exact path. Do not infer it from a different path that happened to survive.
Claim 2: "My in-memory cache and counters are fine."
Fine until the process exits. Then they are zero.
This bites hardest in three places: rate-limit counters, feature flags loaded at boot, and idempotency keys for retried jobs.
Ask the awkward question. If a retry arrives two seconds after a crash, does your dedupe still know?
Expected result: DRIFT on mem. That is correct behaviour, not a bug.
Corrected model: memory is a cache, never a source of truth. Move anything that must outlive a boot to a store that survives it.
Claim 3: "It survived a restart, so it is persistent."
One survival is an anecdote. You need a second class of death.
A process restart and an idle eviction are not the same event. Neither is a redeploy.
Test all three if your platform documents all three. Otherwise you are testing the easy one.
Expected result: a mix. Files may survive restarts and vanish on redeploy.
Corrected model: enumerate the death types, then map which state survives which. A two-column table beats intuition.
Claim 4: "My session is server-side."
The cookie came back, so the login must have survived. Right?
No. The cookie is client state. It proves your HTTP client keeps a jar.
Server-side session data lives in memory or in a store. Restart the process and read it again.
Expected result: PASS on the cookie, DRIFT on whatever backs the session.
Corrected model: separate the token from the record it points at. Probe each one.
Letting a free model label the drift
Now you have a JSON diff and a pile of DRIFT lines. Which ones matter?
That is a policy question, and it is repetitive. Good candidate for a cheap classifier.
Here is the workflow I would use with MonkeyCode's free model access:
- Redact the snapshot JSON. Drop hostnames, IDs, and anything user-specific.
- Paste a short policy: "DRIFT is breaking if it affects auth, money, or job idempotency."
- Ask for one label per line, with a one-sentence reason.
- Keep the labels in the repo next to the probe output.
Do not let a model decide what is acceptable. Let it sort lines you already understand.
The value is triage speed, not authority.
Where each kind of state belongs
| State | Ephemeral OK? | Safer home |
|---|---|---|
| Build output, caches | Yes | Anywhere |
| Rate-limit counters | No, if you bill on them | Shared store |
| Idempotency keys | No | Durable key-value store |
| Session records | Depends on logout cost | Durable store |
| Uploaded files | No | Object storage |
| Feature flags | Yes | Boot-time load |
The right column is a design decision. The probe only tells you where you stand today.
Limitations, and who should skip this
This probe is deliberately shallow. It will not catch partial writes, silent truncation, or clock skew across nodes.
A local docker compose restart is not a platform idle eviction. Read your platform's documented lifecycle before you trust a PASS.
The env var check is weak. Values baked at build time and read from a secrets manager look identical here.
Skip this approach if you already run a managed database with documented durability. Skip it if your compliance model requires an audit trail rather than a probe.
Never run a restart script against a shared host without written permission. And keep PII out of the probe payload entirely.
The checklist I would keep
Before you trust any free server with real users:
- Confirm
boot_idchanges on restart. Otherwise the test is void. - List the death types your platform documents.
- Name every piece of state and where it lives.
- Test the paths that matter, not the convenient ones.
- Re-run the probe after every platform change.
One green run is a data point. Two runs with different death types is evidence.
If you want to try the classification step without a bill, MonkeyCode's free model option is a reasonable place to start — but the probe is the part that will save your weekend.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.