I kept asking the same question for two days: did the overnight client actually reach the model, or did a green health check only flatter me? The notebook on my desk and the process on the server looked like twins, so I treated them as one path. That lazy assumption is what the next forty-eight hours kept punishing. If two processes share a repository, do you still believe they share a clock and a base URL?
I wanted a quiet place to exercise free model access without leaving the laptop awake all night. MonkeyCode's free model access and free server option were the practical fit for that constraint, because I could park the client off my machine. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am not adding quotas, model names, or hardware claims that nobody handed me. The lesson below is about clocks and configuration, and it stays useful if you swap the host.
What I thought the window would prove
I thought a forty-eight hour review would be a simple slice of a log file. I would start the client, sleep, then filter lines whose timestamps sat inside that window. A passing health check would mean the worker was alive, and a few status=ok lines would mean the model path was real. Have you ever trusted a filter before you printed the timezone it assumed?
That plan sounded careful, and it was still wrong in two independent ways. The first bug lived in how I compared times. The second bug lived in which base URL the live process still held. Either bug alone can make a healthy box look silent, or make a silent box look healthy.
What I actually ran
I kept the client boring on purpose. It reads a base URL and a token from the environment, writes one JSON line per attempt, and never prints the token. The server process is the same file, started by a shell script that is supposed to export those variables. Would you bet that the script the supervisor launched is the script you just edited?
python3 --version
date -u +%Y-%m-%dT%H:%M:%SZ
printenv MODEL_BASE_URL
printenv MODEL_TOKEN | awk '{print "set=" (length($0)>0)}'
I used those commands on the server and again in the notebook. Matching versions did not mean matching configuration, which is the mistake I keep relearning. The token check only reports whether a value is present, because pasting secrets into a field note is how reviews leak.
# Worked example you can run locally. Not a captured production trace.
import os
from datetime import datetime, timezone
def call_marker(base_url: str) -> dict:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
host = base_url.split("://", 1)[-1].split("/", 1)[0]
return {
"ts": now,
"event": "model_call",
"base_host": host,
"status": "skipped" if not base_url else "attempted",
}
if __name__ == "__main__":
print(call_marker(os.environ.get("MODEL_BASE_URL", "")))
What broke first: the window
The server wrote timestamps with a Z suffix, which means UTC. My filter built the window from datetime.now() without a timezone, then compared those naive objects to parsed strings. On a machine set to UTC+8, local midnight is not UTC midnight, so the slice quietly shifted by eight hours. Have you checked whether your slice includes tomorrow in UTC and drops yesterday evening?
Older Python also refuses Z inside datetime.fromisoformat, a behavior that changed in 3.11, as the standard library documents. I hit a ValueError, caught it too broadly, and treated the parse failure as "no events." An empty window then looked like a model outage. Was the model down, or did my exception handler erase the only evidence?
from datetime import datetime, timedelta, timezone
def parse_ts(raw: str) -> datetime:
if raw.endswith("Z"):
raw = raw[:-1] + "+00:00"
parsed = datetime.fromisoformat(raw)
if parsed.tzinfo is None:
raise ValueError("naive timestamp rejected")
return parsed.astimezone(timezone.utc)
def in_window(ts: datetime, hours: int = 48) -> bool:
end = datetime.now(timezone.utc)
start = end - timedelta(hours=hours)
return start <= ts <= end
That helper is the piece I would repeat. It rejects naive timestamps instead of guessing a zone. It converts everything to UTC before the comparison, so a laptop in another offset cannot invent a gap. The Python docs for datetime.fromisoformat are the reference I should have opened before blaming the network.
What broke second: the base URL
After the window was honest, lines appeared, and they still surprised me. The health check returned 200 because the process was listening. The log host did not match the host I had just written into .env. Why would a living process notice a file you edited after it started?
I had changed the env file, then curled /health, and never restarted the worker. The old process kept the old MODEL_BASE_URL in its own environment. A green check only proves the socket accepts connections. It does not prove which model endpoint that process will call on the next request.
# Linux process environment, redacted to the key you care about.
pid=$(pgrep -f "python3 client.py" | head -n 1)
tr "\0" "\n" < "/proc/${pid}/environ" | awk -F= '$1=="MODEL_BASE_URL"{print $1"="$2}'
curl -fsS http://127.0.0.1:8080/health
If /proc is unavailable, ask the app to log the host at startup and on SIGHUP. Do not log the token. I would rather restart on purpose than discover a stale URL after a night of cheerful health checks.
A decision table I wish I had opened first
| Signal you saw | What it actually proves | What I would do next |
|---|---|---|
HTTP 200 on /health
|
The process accepts connections | Read the live env or startup host |
| Empty 48-hour slice | The filter found no parsed lines | Print start, end, and raw timestamps in UTC |
ValueError on Z
|
The parser does not match the writer | Normalize Z to +00:00 before parsing |
| Notebook works, server does not | Two environments differ | Diff printenv keys, not secret values |
Host in the log differs from .env
|
The file and the process diverged | Restart, then confirm /proc or startup log |
What I would repeat
I would repeat four checks before I blame the model, the network, or the free option. First I print UTC now on both machines. Second I parse one real log line with the helper above. Third I read the live base host from the process, not from the file I edited. Fourth I record that the token is present without recording the token.
def review(lines: list[str], hours: int = 48) -> dict:
parsed, rejected = [], []
for line in lines:
try:
parsed.append(parse_ts(line.split(" ", 1)[0]))
except ValueError:
rejected.append(line)
kept = [ts.isoformat() for ts in parsed if in_window(ts, hours)]
return {"kept": kept, "rejected": len(rejected), "seen": len(lines)}
Run it against a fixture you commit, not against a story you remember. A two-line fixture with one Z timestamp inside the window and one naive timestamp outside it will fail loudly if someone later "simplifies" the parser. Is a fixture boring? Yes. Did the boring fixture catch the bug I had already shipped into my head? Also yes.
python3 -m pytest window_test.py -q
python3 review_window.py --hours 48 --log worker.log
Label the fixture as a proposal until you have actually executed it on the host you care about. I would rather see a failed assert on a naive timestamp than another night of guessing why the slice is empty.
Limitations, and who should skip this
This workflow does not measure model quality, latency, or how long a free option will exist. I did not publish numbers for those, because I do not have verified measurements to stand behind. A UTC window also will not save you if the log disk was rotated and the lines are already gone. If your host forbids /proc, you need an application status route that prints the host and not the secret.
You should skip this approach if you need an audited production trail, a signed clock, or a promise that free capacity will still be there next week. You should also skip it if you cannot restart the worker, because reading a stale environment without a restart is just a nicer way to stay wrong. Teams that paste tokens into tickets should not adopt the debug commands until they have a redaction habit.
Free model access does not fix a wrong window, and a free server does not refresh an environment you forgot to reload. If the only copy of the log lives on a box you might lose, copy that file off before you start interpreting gaps. A missing file and a bad parser look identical from a chat window.
The part I will not skip next time
Next time I will write the window in UTC before I start the job, and I will restart after every env edit. I will keep the health check, but I will treat it as a pulse, not as proof of routing. If I use that free model access on the free server again, I will still begin with the same four checks rather than with a theory about the model.
A green light is a question. The live clock and the live base URL are the answer I should have demanded on hour one. Would I trust the next overnight run without those two prints? Not a chance.
Top comments (0)