You push. The build goes green. Then it 502s.
Sound familiar? The gap is almost never your business logic. It is the environment you quietly assumed.
So here is a myth-busting FAQ about free tiers. Claims developers repeat. Evidence you can collect yourself. A corrected mental model after each one.
Two claims are on the table, and only two. MonkeyCode offers free model access, and it has a free server option. Those are operator-supplied availability claims.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Everything else — quotas, hardware, sleep policy, storage lifetime — stays unmeasured until you probe it. I have not run the code below against that server for this article. You are the one collecting numbers.
Why "works on my machine" is a measurement problem
Your laptop and any remote host are different computers. That is not a bug. It is a fact you forgot to write down.
When something breaks on deploy, you are not debugging code. You are debugging unstated assumptions. So stop guessing and start enumerating.
Myth 1: "Same app, same environment"
The claim: I run the same build, so behavior must match.
But env vars differ. Working directories differ. The process supervisor differs. Even the timezone and locale may differ quietly.
A 12-factor app reads config from the environment. That does not mean both environments hold the same config.
Corrected model: a deployment is a new environment, not a copy of yours. Enumerate the facts you depend on, then assert them at boot.
- Which env vars are present? Presence matters more than value.
- What is
process.cwd()? Relative paths resolve against it. - Is the directory writable? Many apps assume yes.
Myth 2: "If it binds locally, it's reachable"
The claim: the port works on my machine, so the host will expose it.
Locally you often bind 127.0.0.1. That is loopback only. A container or VM needs 0.0.0.0 to accept outside traffic.
Also many platforms inject PORT. Hardcoding 3000 is a guess dressed as a default.
Corrected model: binding is a two-part contract — address and port. Read both from the environment. Verify from outside, not inside.
Myth 3: "A restart is a no-op"
The claim: restarting just reruns my process, so nothing changes.
But in-memory state vanishes. Counters reset. Any local file cache might vanish too, depending on where you wrote it.
Write to /tmp? Maybe ephemeral. Write to cwd? Maybe not writable at all.
Corrected model: assume nothing survives a restart. Then prove which parts do. Persistence is a measurement, not a vibe.
Myth 4: "Free model access means free inference capacity"
The claim: the endpoint answers, so throughput is a solved problem.
Access is not capacity. A reachable endpoint tells you one thing only: it answered once, from one place, at one moment.
So measure rather than assume. Latency from your laptop is not latency from the host. Rate behavior under concurrency is unknown until you push.
Corrected model: treat free access as an envelope you discover. Send a small burst, record outcomes, and set your own concurrency ceiling below it.
Myth 5: "Local latency predicts user latency"
The claim: my API call takes 300ms at home, so users see 300ms.
Your laptop sits close to your network. The host sits somewhere else, with its own route and its own queue.
Add cold start on top. The first request after idle is not the thousandth request in steady state.
Corrected model: latency is a property of a path, not of a function. Measure from where the code runs.
The artifact: a /probe endpoint and a restart test
Here is the smallest thing that converts assumptions into evidence. One file, zero dependencies, Node 18+.
// probe.js — run: PORT=3000 node probe.js
import { createServer } from "node:http";
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
const bootId = randomUUID();
const startedAt = Date.now();
const markerPath = join(tmpdir(), "probe-marker.json");
const target = process.env.PROBE_TARGET ?? "";
function writable(dir) {
try {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, `.probe-${bootId}`), "ok");
return true;
} catch { return false; }
}
function previousBoot() {
if (!existsSync(markerPath)) return null;
try { return JSON.parse(readFileSync(markerPath, "utf8")); } catch { return null; }
}
async function upstream() {
if (!target) return { skipped: true };
const t0 = performance.now();
try {
const r = await fetch(target, { method: "HEAD" });
return { status: r.status, ms: Math.round(performance.now() - t0) };
} catch (e) {
return { error: String(e?.message ?? e), ms: Math.round(performance.now() - t0) };
}
}
const server = createServer(async (req, res) => {
if (req.url !== "/probe") { res.writeHead(404).end("not found"); return; }
const prev = previousBoot();
writeFileSync(markerPath, JSON.stringify({ bootId, startedAt }));
const body = {
bootId,
uptimeSec: Math.round((Date.now() - startedAt) / 1000),
node: process.version,
platform: process.platform,
env: { PORT: process.env.PORT ?? null, NODE_ENV: process.env.NODE_ENV ?? null },
files: {
cwd: process.cwd(),
cwdWritable: writable(process.cwd()),
tmp: tmpdir(),
tmpWritable: writable(tmpdir())
},
previousBoot: prev,
upstream: await upstream()
};
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify(body, null, 2));
});
server.listen(Number(process.env.PORT ?? 3000), "0.0.0.0", () => {
console.log(`probe on 0.0.0.0:${process.env.PORT ?? 3000}`);
});
Note the deliberate choices. It binds 0.0.0.0. It reports env var presence, never values. It returns no secrets.
Run the restart test
Now call it twice, with a restart in between.
curl -s https://YOUR_HOST/probe | tee probe-1.json
# restart the service however your host does it
curl -s https://YOUR_HOST/probe | tee probe-2.json
diff <(jq -S . probe-1.json) <(jq -S . probe-2.json)
Then read the diff like a diagnostic.
-
bootIdchanged? Good, the process really restarted. -
previousBootisnull? That marker did not survive. Your/tmpis not durable. -
cwdWritableisfalse? Drop file-based caches from that path. -
upstream.msis large or errors? Measure from the host, not your desk.
Set PROBE_TARGET to your model endpoint URL to include it. A HEAD may return 405; that is still a useful reachability signal.
Guard this endpoint
A probe that leaks paths is a reconnaissance gift. Gate it behind a header or remove it after the test.
Never reuse /probe as your production health check. It writes files. Health checks should not.
Decision table: what belongs where
Once the probe answers, placement stops being a debate. Use this table.
| Workload | Your laptop | Free server option | Why |
|---|---|---|---|
| Public HTTP endpoint | No | Yes | Needs an always-on address |
| Long cron batch | No | Verify first | Restarts kill in-flight work |
| Local file cache | Yes | Only if writable | Probe tells you |
| API key storage | Never | Env vars | Never in the repo |
| Model calls under burst | No | Measure first | Access is not capacity |
| Anything with an SLA | No | No | Free is not a contract |
Read the last row twice. Free is a starting point, not a promise.
Limitations and who should not use this
This probe reports facts about one process at one instant. It is not monitoring. It will not catch a host that degrades next Tuesday.
It says nothing about model output quality either. That is a separate experiment.
Skip this approach if you handle regulated data and cannot expose a diagnostics route. Skip it if you need a durability guarantee in writing.
Also skip it if you cannot accept a restart wiping state. Pick a platform with durable storage instead of fighting the free one.
The one-line takeaway
Stop asking "why did it work locally?" Start asking "what did I never verify?"
Write the probe. Run it twice. Let the diff argue for you. If you want a cheap place to point the probe at a model endpoint, MonkeyCode's free model access and free server option are enough to start measuring — and you now know exactly what to measure.
Top comments (0)