What does a server do when nobody is calling it, and what happens the moment that changes? I spent 48 hours with a free server, a health endpoint, and a tiny polling script to answer exactly that question. Free-tier infrastructure is famous for silent cold starts, and dashboard uptime tells you how long the process claims to have lived, not how many lives it already had. So I built a restart detector that could not lie: a boot id.
Sleep studies record the moments when breathing stops. Free-tier deployments have a similar tell: the process disappears, and a minute later everything looks normal again, with zero traces of the gap. Timestamps alone will not save you because clocks can jump and schedulers can freeze a process without killing it. The only artifact that survives a restart is the one you deliberately wrote before the restart happened.
Why a Sleep Study
The word "sleep" fits free stuff better than you think. A free server is shared hardware on borrowed time, and nobody promises when the scheduler takes that time back. A minute after a hiccup, you reconnect and see a perfectly healthy API, so the incident disappears unless you built a witness. My witness is a boot id, a random UUID minted at process start and written into every health response and every startup log line.
This harness is deliberately small so you can reproduce it anywhere. It is a single-file Node service, a polling loop, and a log file, none of which depends on a specific vendor. You can point the same script at any HTTP endpoint you deploy to, and the same signals will appear.
The Deliberately Boring Service
// server.js
const http = require("http");
const crypto = require("crypto");
const bootId = crypto.randomUUID();
const bootedAt = Date.now();
http
.createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
bootId,
uptimeMs: Date.now() - bootedAt,
now: new Date().toISOString(),
})
);
return;
}
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("ok");
})
.listen(process.env.PORT || 3000, () => {
console.log(`[boot] ${bootId} up at ${new Date().toISOString()}`);
});
Run it with PORT=3000 node server.js and the boot line appears once. Keep that line in your logs and the boot id in your response, and now every health check can prove which generation of the process answered.
The Polling Script
The observer stays quiet during normal operation and prints only anomalies. I poll every five seconds with a ten-second timeout, so a missed window is visible even when the next request succeeds. Silence, in other words, becomes part of the report.
#!/usr/bin/env bash
set -u
URL="${HEALTH_URL:-http://localhost:3000/health}"
INTERVAL="${POLL_INTERVAL:-5}"
PREV=""
while true; do
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
body=$(curl -sS --max-time 10 "$URL" 2>/dev/null) || {
echo "$ts UNREACHABLE"
sleep "$INTERVAL"
continue
}
boot=$(printf '%s' "$body" | grep -o '"bootId":"[^"]*"' | cut -d'"' -f4)
if [ -z "$boot" ]; then
echo "$ts BAD_JSON: ${body:0:80}"
elif [ -n "$PREV" ] && [ "$boot" != "$PREV" ]; then
echo "$ts RESTART: $PREV -> $boot"
fi
PREV="$boot"
sleep "$INTERVAL"
done
Three kinds of lines can come out of this script, and each one means something different.
Reading the Three Signals
UNREACHABLE means the endpoint disappeared entirely. The request timed out, the connection was reset, or the listener simply went away, and this is usually the easiest symptom because your own tooling already caught it. Check whether a new boot id appears after recovery; if the id stays the same, you saw a hiccup rather than a death.
RESTART with a fresh boot id is the real headline. If a new UUID shows up in the response, the old process died and a replacement started, regardless of what the platform dashboard claims. The uptimeMs field drops to near zero, which is the second tell you want to record alongside the id.
BAD_JSON is the sneaky one: the endpoint honestly returns something, but it is not your application. A login page, an HTML error, or a cached route can look like a healthy response to a blind caller. Printing the first characters of the body is what turns that misleading success into a diagnosable line.
Your Quiet Log Is Also a Finding
My most honest session produced zero anomaly lines. Forty-eight hours of silence sounds boring, and it is, but boredom is the point: silence proves the harness saw a process that never changed. I only trusted that negative result after I forced a restart myself, just to confirm the detector could catch it.
kill -9 "$(lsof -ti tcp:3000)" && node server.js
The next poll printed RESTART with a fresh boot id, and that test injection means an empty log now carries meaning. If you skip this step, an empty log proves nothing. It could be a broken script instead of a stable server, and you would have no way to tell the difference.
Would I Repeat These Four Habits
Yes, and I would freeze them into a checklist for the next deployment.
- Put a boot id in every response and every startup log. It costs three lines of code and gives you restart detection that cannot be fooled by clock jumps.
- Poll faster than your timeout. If your client tolerates five seconds, polling every thirty seconds might completely miss an outage window.
- Save a snippet of the raw body on bad responses. The first eighty characters of an HTML login page are worth a thousand guesses.
-
Test your own detector with
kill -9. A negative result only proves your script works after the script has demonstrated that it can scream.
What This Taught Me About Free Servers
The most important lesson is that uptime and lives are different measurements. A server can show "seventeen days" of uptime and still run a process that was born a minute ago. My boot id makes that distinction impossible to miss, and once I saw a restart as a normal line in a log, I stopped treating it like a crisis.
MonkeyCode offers free model access and a free server option, which means exactly this kind of experiment can run without a credit card. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The harness itself is generic, though: change the health URL and the same sleep study works on any platform you are evaluating.
Who Should Not Use This Approach
This workflow is for stateless services and lightweight verification, not for pager-duty reliability. If you are running a live customer-facing system, your survival plan needs a managed orchestration layer, region redundancy, and automated failover, not a bash loop. Similarly, if your process keeps state in memory, a restart detector will tell you when it happened but it will not rebuild the lost state. Use this to choose platforms and to smoke-test deployments, and leave the hard guarantees to the tools built for them.
A free server will reset at the least convenient moment, and the only real cost is surprise. Once the boot id lives in your logs, surprise turns into a normal line, and forty-eight hours of sleep study turns into the cheapest documentation you ever wrote. I will keep running this test before I trust any free tier with real traffic. This felt like a very quiet field note, and it turned out to be the loudest one yet.
Top comments (0)