DEV Community

Quinn Wang
Quinn Wang

Posted on

The Slow Step Should Never Wait Its Turn

Fifteen minutes is a budget, not a vibe, and the fastest way to blow it is to schedule your slowest step last. I keep meeting the same first-run experience: three correct steps, written in the wrong order, defended by nobody. The steps are usually fine — the schedule is what leaks. So this is a post about ordering, with a harness you can point at your own setup tonight.

The loop I care about is deliberately small: prove the key works, get one reply from a model, get one answer from a server. Free model access and a free server option make that loop cheap enough to restart five times in an afternoon, which is exactly what a first-run budget needs. The setup I am pointing the harness at here uses MonkeyCode's free model access and free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Here is the naive version, and yes, it is the version I wrote first. Three awaits in a column, each waiting for the one above it, even though almost nothing about them depends on each other.

// serial.mjs — the shape of most quickstarts, including my own first draft
const key    = await checkAuth();     // one request
const reply  = await askModel();      // one request, but only after auth resolves
const health = await bootServer();    // a cold start, but only after the model replies
Enter fullscreen mode Exit fullscreen mode

Ask the obvious question out loud: does the server boot need the model reply before it can begin? No. Does auth need the boot at all? Also no. So why is the slowest stage sitting at the bottom of the file? Because it was written last, and nobody re-read the file with a stopwatch in hand.

That is the whole teardown, and the fix that mattered is one line of scheduling, not three new features.

// the fix: the slow, idempotent stage starts first and runs beside the fast one
const [bootResult, authResult] = await Promise.all([boot(), auth()]);
const modelResult = authResult.status === "ok"
  ? await model()
  : { stage: "model", status: "skipped", ms: 0, note: "auth failed first" };
Enter fullscreen mode Exit fullscreen mode

Wall clock goes from boot + auth + model to roughly max(boot, auth) + model. If your cold boot is the long pole — and it usually is, because a free server option will happily cold-start for you — you have just removed almost all of it from the critical path. If the boot is not the long pole, the harness tells you so and you delete that branch. Either way you stop guessing.

Here is the full harness, no dependencies, Node 18 or newer. Every stage has a name, its own budget, and an abort handle, so a hang becomes a labelled timeout instead of a silent stare at the terminal.

#!/usr/bin/env node
// envelope.mjs — three first-run probes under one 15-minute envelope.
// Env: MODEL_BASE_URL, MODEL_API_KEY, MODEL_NAME, SERVER_HEALTH_URL
// Optional: BUDGET_AUTH_MS, BUDGET_MODEL_MS, BUDGET_BOOT_MS, ENVELOPE_BUDGET_MS, LEDGER_PATH
import { writeFileSync } from "node:fs";

const BUDGET_MS = Number(process.env.ENVELOPE_BUDGET_MS ?? 900_000);
const STAGE_BUDGET = {
  auth:  Number(process.env.BUDGET_AUTH_MS  ?? 60_000),
  model: Number(process.env.BUDGET_MODEL_MS ?? 120_000),
  boot:  Number(process.env.BUDGET_BOOT_MS  ?? 240_000),
};

const t0 = performance.now();
const elapsed = () => Math.round(performance.now() - t0);

// Never throws: a failed stage returns a failed row, so one error cannot hide the others.
async function stage(name, fn) {
  const budget = STAGE_BUDGET[name];
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), budget);
  const started = performance.now();
  try {
    await fn(ctrl.signal);
    return { stage: name, status: "ok", ms: Math.round(performance.now() - started), note: "" };
  } catch (err) {
    const status = err.name === "AbortError" ? "timeout" : "error";
    return { stage: name, status, ms: Math.round(performance.now() - started), note: String(err.message ?? err) };
  } finally {
    clearTimeout(timer);
  }
}

const auth = () => stage("auth", async (signal) => {
  const key = process.env.MODEL_API_KEY;
  if (!key) throw new Error("MODEL_API_KEY unset — this is an auth failure, not a model failure");
  const res = await fetch(`${process.env.MODEL_BASE_URL}/models`, {
    headers: { authorization: `Bearer ${key}` }, signal,
  });
  if (!res.ok) throw new Error(`auth endpoint said ${res.status}`);
});

const model = () => stage("model", async (signal) => {
  const res = await fetch(`${process.env.MODEL_BASE_URL}/chat/completions`, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${process.env.MODEL_API_KEY}` },
    body: JSON.stringify({
      model: process.env.MODEL_NAME,
      messages: [{ role: "user", content: "Reply with the single word: ready" }],
      max_tokens: 8,
    }),
    signal,
  });
  if (!res.ok) throw new Error(`model endpoint said ${res.status}`);
});

const boot = () => stage("boot", async (signal) => {
  const res = await fetch(process.env.SERVER_HEALTH_URL, { signal });
  if (!res.ok) throw new Error(`health endpoint said ${res.status}`);
});

// Schedule: the slow idempotent stage goes first, beside auth. Model waits for auth only.
const [bootRow, authRow] = await Promise.all([boot(), auth()]);
const modelRow = authRow.status === "ok"
  ? await model()
  : { stage: "model", status: "skipped", ms: 0, note: "auth failed first" };

const rows = [authRow, modelRow, bootRow];
const width = Math.max(...rows.map(r => r.stage.length));
for (const r of rows) {
  console.log(`${r.stage.padEnd(width)}  ${r.status.padEnd(7)}  ${String(r.ms).padStart(6)}ms  ${r.note}`);
}

const total = elapsed();
const bad = rows.filter(r => r.status !== "ok");
console.log(`\nverdict: ${bad.length ? "outside — " + bad.map(b => b.stage).join(", ") : "inside the envelope"}`);
console.log(`wall clock ${(total / 1000).toFixed(1)}s of a ${(BUDGET_MS / 1000).toFixed(0)}s budget`);

writeFileSync(process.env.LEDGER_PATH ?? "envelope-ledger.json",
  JSON.stringify({ runAt: new Date().toISOString(), totalMs: total, rows }, null, 2));

process.exitCode = bad.length === 0 && total <= BUDGET_MS ? 0 : 1;
Enter fullscreen mode Exit fullscreen mode

Run it with the cheapest possible inputs, and do not commit the key:

MODEL_BASE_URL="$BASE" MODEL_NAME="$NAME" SERVER_HEALTH_URL="$HEALTH" \
  MODEL_API_KEY="$(cat ~/.secrets/trial-key)" node envelope.mjs

echo "exit code: $?"   # 1 means the envelope was missed, and the rows say where
Enter fullscreen mode Exit fullscreen mode

The output format matters more than my numbers, which I am not going to pretend to know for your network. It looks roughly like this, with NN.Ns standing in for whatever your machine actually does:

auth    ok         NN.Ns
model   ok         NN.Ns
boot    ok         NN.Ns

verdict: inside the envelope
wall clock NN.Ns of a 900s budget
Enter fullscreen mode Exit fullscreen mode

Now the part I actually care about: the ledger file behind the verdict. One run is an anecdote, but six runs on six mornings answer a much better question — is this stage slow, or was that one Tuesday slow? Compare totalMs across the file and read the row that keeps winning. Blaming the model for a cold boot is the most common misdiagnosis I have watched people make, and it costs them an afternoon of prompt tweaking that was never going to help.

Long pole in the ledger First move
boot dominates Start it first and let it warm while auth runs; consider a keep-warm ping before you sit down
auth dominates Cache the exchange locally for the session; a 401 loop is not a model problem
model dominates Shrink the probe to a two-token reply before you judge latency
Nothing dominates, but the total is over The envelope is wrong, not the provider; write down what you actually need

A few honest limitations, because this harness is a stopwatch, not a diagnosis. Free tiers queue, rate-limit, and change underneath you, so a green run today is evidence about today only. The parallel branch assumes your server boot is idempotent and safe to cancel, which is not true for every service, and behind a corporate proxy you are mostly measuring the proxy. And the script proves which stage is slow without telling you why it is slow — you still have to read a log.

Who should skip this? Teams on committed infrastructure with stable latency and real monitoring, because the ledger is a downgrade from what they already have. Anyone whose first fifteen minutes are dominated by a compiler toolchain, since the harness will just confirm a truth you already resent. And anyone who wants to turn this into a leaderboard — it measures your network and your order of operations, not the quality of anybody's model.

If you want something free to break while you test the schedule, the free model access and free server option are a fair place to point the harness; check the current quotas in your own dashboard rather than trusting a number in a blog post, mine included. Then move one await and watch the wall clock change shape, because that is the whole trick.

Top comments (0)