DEV Community

Jordan Huang
Jordan Huang

Posted on

Four Idle Myths About Free Servers, and the Wake Probe That Settles Them

Someone pings your free server every five minutes. They swear it stays fast. Do you believe them?

I stopped believing. Not because the claim is obviously wrong. Because nobody in the thread had measured it.

So I built a small probe instead. It answers the question with timestamps, not folklore. Here are four idle myths I keep hearing, and the check that ends each one.

The claims I keep reading

  • "Just hit /health every few minutes. The box never sleeps."
  • "If /health returns 200, the app is ready."
  • "Idle costs nothing, so keep-alive is free."
  • "My scheduled job runs at 03:00. It's in the config, right?"

Each one sounds reasonable. Each one is a guess about a system you do not control.

Sleep policy is platform-specific. It can change without a changelog you read. Read your provider's docs first, then measure your own deployment.

Myth 1: A ping every five minutes keeps the instance warm

What is "warm," exactly? Warm for HTTP is not warm for your database pool. Warm for the pool is not warm for the model call path.

A keep-alive ping proves one narrow thing. Something accepted a TCP connection recently. It does not prove your cold path is gone.

And the ping is not free of side effects. It can consume request quota. It can keep logs noisy. It can even hold the instance in a state your provider bills differently.

Corrected model: a ping is a heartbeat, not a thermometer. Measure the latency you actually serve, not the fact that a socket answered.

Myth 2: If /health returns 200, the app is ready

I have shipped this mistake more than once. /health returned 200 while the first real request took far longer.

Why? The health route was cheap. It touched no dependency. It proved the process was alive, and nothing more.

Split liveness from readiness. Liveness says "the process is up." Readiness says "the next request can be served."

// Illustrative example. Not executed here. Adapt paths to your app.
app.get('/live', (req, res) => res.status(200).send('ok'));

app.get('/ready', async (req, res) => {
  const checks = {};
  checks.db = await pingDb();        // your own helper
  checks.downstream = await pingModel(); // your own helper
  const ok = Object.values(checks).every(Boolean);
  res.status(ok ? 200 : 503).json({ ok, checks });
});
Enter fullscreen mode Exit fullscreen mode

Point your keep-alive at /ready, not /live. A 503 from /ready is information. A 200 from /live is a shrug.

Myth 3: Idle time is free

Free tiers rarely bill idle in currency. They bill it in other ways.

  • Request quotas burned by pings you never needed.
  • Log lines that bury the one error you care about.
  • A warm instance that hides a cold-start bug until real users arrive.
  • Cron runs that overlap because the previous one never finished.

Ask a blunt question. Is the keep-alive protecting users, or protecting you from seeing a number?

Corrected model: idle has a cost. It is just paid in quota, observability, and surprise.

Myth 4: Scheduled jobs run on schedule on a sleeping instance

Your config says 03:00. Your instance may not be listening at 03:00.

In-process schedulers need a running process. If the process is asleep, the timer is not pending anywhere. It is just gone.

Check three things before you trust a schedule:

  1. Where does the timer live? In the process, or in an external scheduler?
  2. What happens to a missed run? Retry, skip, or silent drop?
  3. Can the job start twice? Make it idempotent either way.

The wake probe

Here is the check I run. It is boring on purpose.

It sends a request, waits a controlled idle window, sends another, and records both. Run it from a machine outside your host. localhost timing proves nothing about the network.

#!/usr/bin/env bash
# Illustrative. Not executed here. Point URL at a read-only endpoint.
URL="https://your-app.example/ready"
OUT="wake-probe.jsonl"

hit() {
  local mode="$1" idle="$2"
  local ttfb status
  read -r ttfb status < <(curl -s -o /dev/null \
    -w '%{time_starttransfer} %{http_code}\n' \
    --max-time 60 "$URL")
  printf '{"ts":"%s","mode":"%s","idle_s":%s,"ttfb_s":%s,"status":%s}\n' \
    "$(date -u +%FT%TZ)" "$mode" "$idle" "$ttfb" "$status" >> "$OUT"
}

# Warm baseline: five back-to-back requests
for i in 1 2 3 4 5; do hit warm 0; sleep 2; done

# Wake cycles: increase the idle window, one request each
for idle in 300 900 1800; do
  echo "sleeping ${idle}s"
  sleep "$idle"
  hit wake "$idle"
done
Enter fullscreen mode Exit fullscreen mode

Then summarize. Do not eyeball the raw lines.

// Illustrative. Not executed here. Node 18+, no dependencies.
import { readFileSync } from 'node:fs';

const rows = readFileSync('wake-probe.jsonl', 'utf8')
  .trim().split('\n').map((l) => JSON.parse(l));

const median = (xs) => xs.sort((a, b) => a - b)[Math.floor(xs.length / 2)];

const warm = rows.filter((r) => r.mode === 'warm').map((r) => r.ttfb_s);
const base = median(warm);

console.log('warm median ttfb:', base.toFixed(3), 's');
for (const r of rows.filter((r) => r.mode === 'wake')) {
  const ratio = r.ttfb_s / base;
  console.log(`idle ${r.idle_s}s -> ${r.ttfb_s.toFixed(3)}s (${ratio.toFixed(1)}x) status ${r.status}`);
}
Enter fullscreen mode Exit fullscreen mode

Run one cycle first. Do not hammer a free endpoint. If you see 429, stop and wait. That is the platform telling you something useful.

Reading the result

The numbers are yours, not mine. I will not hand you a benchmark. My wake penalty says nothing about your app.

What you are looking for is shape, not precision:

  • Does the penalty grow with the idle window, or stay flat?
  • Does it appear after one minute, or after thirty?
  • Does /ready return 503 during the wake, or 200 with a slow body?
  • Does the penalty disappear on the second request in a row?

That last one matters most. A penalty that vanishes on request two is a cold-start story. A penalty that persists is something else.

What to do with the answer

Wake penalty Traffic shape Reasonable move
Small, flat Bursty Accept it. Do not add a keep-alive.
Small, flat Steady Measure again under real load before deciding
Large, first request only Steady Warm the specific cold path, not the socket
Large, every request Any Stop; this is not a cold-start problem
429 or 5xx during wake Any Back off, then read the docs

Notice what is missing. No row says "ping every five minutes." Keep-alive is one option, not the default.

Corrected mental model: you do not warm an instance. You warm the specific path a user is about to walk.

Where MonkeyCode actually fits here

My probe checks a downstream dependency, usually a model call. That call has its own cold path and its own failure modes.

MonkeyCode's free model access and free server option are useful for exactly this kind of throwaway measurement rig. I get a place to run the probe, and a downstream endpoint to put in the /ready check. Operator-supplied availability, not a benchmark promise. Your quotas and limits are whatever your account shows.

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

One honest caveat. If you need guaranteed warm capacity, a free tier is the wrong tool. Buy the guarantee. Do not build a ping farm around a policy you cannot see.

Limitations, and who should skip this

  • This measures one vantage point at one time, not your users' median.
  • Provider sleep policy can change. Re-run the probe after any platform notice.
  • A single probe conflates DNS, TLS, proxy, and app startup time.
  • Read-only endpoints only. Do not wake a box that writes on GET.

Skip this approach if you are already on paid always-on capacity. Skip it if your endpoint is not safe to call repeatedly. Skip it if you cannot stop when you see 429.

The short version

A ping is not a thermometer. A 200 from /health is not readiness. Idle is not free. A sleeping box does not run your cron.

Four myths, one probe, and about an hour of waiting. That is the whole trick.

What is your wake penalty? You cannot know until you time it. Run one cycle, read four lines, and let the graph argue for you next time.

Top comments (0)