DEV Community

Avery Lin
Avery Lin

Posted on

Measure the Cold Start Before the Demo

An indie founder shipped a demo at 23:40 on a Sunday. The link went into a direct message five minutes later. The first request took thirty-eight seconds. The visitor closed the tab before the spinner stopped.

Nothing was broken. The free host had gone to sleep. That is the whole failure mode: the first request is the one a stranger judges.

Free hosts sleep when idle. Free model endpoints throttle under bursts. Both give way at the same moment, which is the first honest request from someone who is not the author. A warm browser tab hides it. A local run hides it too.

So the useful check is not whether the app works. It is how long the app takes after ten quiet minutes. That number is small, measurable, and it fits in one shell script.

Probe the first request

The script below measures three things against a deployed URL. It records a warm hit, waits for the host to fall asleep, then records the first hit after the sleep. A third probe touches the route that calls a model on the server.

#!/usr/bin/env bash
# probe.sh — warm, cold and model-path latency for one deployed URL.
set -uo pipefail

BASE="${1:?usage: ./probe.sh https://your-app.example.com}"
IDLE="${IDLE:-420}"        # seconds to let the free host fall asleep
OUT="${OUT:-probe.tsv}"

run() {
  local label="$1" url="$2" res code ttfb total
  res=$(curl -sS -o /dev/null --max-time 90 \
        -w '%{http_code} %{time_starttransfer} %{time_total}' "$url" 2>/dev/null) \
        || res='000 0 0'
  read -r code ttfb total <<<"$res"
  printf '%s\t%s\t%s\t%s\t%s\n' \
    "$label" "$(date -u +%FT%TZ)" "$code" "$ttfb" "$total" | tee -a "$OUT"
}

: > "$OUT"
printf 'label\ttime\tcode\tttfb_s\ttotal_s\n' | tee -a "$OUT"

run warmup    "$BASE/healthz"
run warmup_ui "$BASE/"
sleep "$IDLE"
run cold      "$BASE/healthz"
run cold_ui   "$BASE/"
run model     "$BASE/api/ping"   # one short server-side model call
Enter fullscreen mode Exit fullscreen mode

Run it three times, not once. A single sample is noise, and a sleeping host is slow in bursts.

The /healthz route should render a fixed string and touch nothing else. A health check that queries a database measures the database, not the sleep penalty. The /api/ping route sends one short prompt through the server-side model call and returns a fixed marker.

Read the numbers, not the dashboard

The columns carry different meanings. time_starttransfer is the first byte, which is what the visitor perceives as the spinner stopping. time_total includes the rest of the body, so a large gap between the two points at a heavy payload instead of a cold process.

A one-line comparison turns the file into a verdict.

awk -F'\t' '
  $1=="warmup" {w=$5}
  $1=="cold"   {c=$5}
  $1=="model"  {m=$5}
  END {printf "warm %.2fs | cold %.2fs | model %.2fs | cold/warm %.1fx\n", w, c, m, c/w}
' probe.tsv
Enter fullscreen mode Exit fullscreen mode

The ratio matters more than the absolute number. A warm request at 0.3s and a cold one at 12s is a 40x cliff. The app is fine at 12s for a crawling bot and fatal for a person with a phone in one hand.

cold total what the visitor sees what to do
under 2s a normal page ship the link
2 to 10s a short wait pre-warm 60s before sending it
10 to 45s a dead tab serve a static landing page first
timeout or 5xx an error fix the app, not the host

That table is a decision aid, not a benchmark. Thresholds move with the audience. A demo sent to one investor tolerates more waiting than a link posted in a public thread.

Warm it before the link, not after

Scheduled pings are the obvious fix and the fragile one. Cron services and CI schedules fire late under load, often by several minutes. Some free tiers also treat artificial keep-alive traffic as abuse, so read the terms before adding a pinger.

The cheaper move is a manual pre-warm. Hit the URL once, sixty seconds before the link goes out, from the same laptop that sends it. That single request moves the visitor from a cold tab to a warm one, and it costs nothing.

The second move is honesty in the UI. A page that says it is starting beats a blank screen that looks dead. A static shell that renders in 200ms and fills in later keeps the visitor reading while the process wakes.

The model call shares the same budget

A sleeping host and a slow model compose badly. The request that wakes the process is also the request that waits on a remote completion. Two queues stack in one tab.

Keep the model path to a single round trip during a demo. Ask for a short answer, set a hard client timeout, and never chain two completions in the same click. The probe above measures that path so the number is known before a stranger finds it.

Repeated demo clicks should not cost anything either. A small file cache keyed by prompt hash removes the second call entirely.

import { createHash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';

const dir = '.cache/answers';
const key = (p) => createHash('sha256').update(p).digest('hex').slice(0, 16);

export async function answer(prompt, call) {
  await mkdir(dir, { recursive: true });
  const path = `${dir}/${key(prompt)}.json`;
  try {
    return JSON.parse(await readFile(path, 'utf8'));
  } catch {
    // cache miss, fall through to the model
  }
  const out = await call(prompt);
  await writeFile(path, JSON.stringify(out));
  return out;
}
Enter fullscreen mode Exit fullscreen mode

That cache lives on the same disposable disk as the app. It dies with a restart, which is acceptable for a demo and wrong for a record of anything a customer paid for.

Limits that survive the fix

A free host still sleeps on its own schedule. It runs in one region, behind one IP, with no commitment to uptime. A probe run at 02:00 says nothing about 14:00 on a launch day.

Free model access has its own ceiling. Throughput is shared, quality varies by task, and the allowance can change without a migration path. The operator of the MonkeyCode project describes a free token allowance, quoted at ten million tokens at the time of writing, plus a free server option. Treat both as time-sensitive facts and confirm them on the project page rather than in a blog post. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Anyone with an uptime clause, an inbound webhook with tight timing, or regulated data should not build here. Paying customers behind a host that naps is a support queue, not a business. Teams that need a GPU or a fixed IP should look elsewhere too.

The indie case is narrower and honest. One builder, one demo, one week of attention, a bill of zero. Ship today, keep the number small, and accept the ceiling out loud.

The probe costs ten minutes. The demo link costs one click that cannot be taken back.

If a zero-bill stack is the goal, the free model access and free server option in MonkeyCode are worth a look. Run the probe first, then decide.

Top comments (0)