cd /news/ai-infrastructure/the-disconnect-test-a-take-home-wher… · home › topics › ai-infrastructure › article
[ARTICLE · art-151164] src=dev.to ↗ pub= topic=ai-infrastructure verified=true sentiment=· neutral

The Disconnect Test: A Take-Home Where Cancelling Upstream Is the Real Requirement

A developer published a take-home interview exercise that tests whether candidates building a streaming proxy in front of an OpenAI-compatible /v1/chat/completions endpoint correctly abort the upstream request when the client disconnects. The reference solution uses Node's built-in fetch with one AbortController per upstream attempt and a deterministic fake upstream exposing started/completed/aborted counters, so graders can verify cancellation at the process boundary without network access. The writeup argues that leaked upstream calls keep sockets open, hold shared endpoint capacity, and generate tokens nobody reads, making cancellation the difference between a proxy that scales and one that accumulates debt.

by read8 min views1 publishedOct 16, 2026

A streaming proxy take-home rarely fails on the happy path, because almost every candidate can pipe bytes from an upstream endpoint into an HTTP response. The graded behaviour appears when the client disappears halfway through the stream and the upstream request has to die with it. Most submissions leak that upstream call, keep generating tokens that nobody will read, and never notice, because their own test suite exercises only the successful case. This article gives the prompt, a reference solution, a weighted rubric, and the failure modes worth grading.

The exercise stays intentionally small: one route, one upstream dependency, one process. Candidates receive the following brief and roughly two hours to return code plus a short written note.

Implement POST /chat as a streaming proxy in front of an OpenAI-compatible /v1/chat/completions endpoint.

  • Stream the upstream body to the client without buffering the whole response.
  • Abort the upstream request when the client disconnects.
  • Retry only before the first byte reaches the client, never afterwards.
  • Use an idle timeout rather than a total-duration timeout.
  • Emit one structured log line per request with status, bytes, and outcome.
  • Ship a test that proves the cancellation path without network access.

The third and fourth bullets carry most of the signal, because they encode a specific mental model of stream lifetime. A candidate who treats the upstream call as a fire-and-forget side effect will pass everything else and fail those two. The deliverable is deliberately narrow so that graders compare designs rather than feature counts.

Every leaked upstream request keeps a socket open, holds a slot on a shared endpoint, and continues generating tokens that no user will read. On a metered endpoint that leak shows up directly on the invoice, and on a shared free endpoint it degrades the experience for everyone else using the same capacity. Cancellation is therefore not a nicety but the difference between a proxy that scales and one that quietly accumulates debt.

The same reasoning explains the no-retry-after-first-byte rule. Once a token has reached the client, a retry produces a second stream spliced into the first, which surfaces as duplicated or interleaved text. Graders look for that duplication because it is one of the few failures a reviewer can spot from outside the process.

Two counters on a fake upstream settle the question faster than any code review. Start a stream, abort the client after a couple hundred milliseconds, then read the counters back.

curl -s localhost:8787/__stats   # {"started":2,"completed":1,"aborted":1}

An aborted count that matches the number of killed clients means the design is sound at the process boundary. A zero there means the upstream call outlived its consumer, and the rest of the submission is mostly commentary.

The reference uses the built-in fetch, one AbortController per upstream attempt, and a deterministic fake upstream so the tests never touch a real endpoint. It is a spec-complete sketch rather than a polished service, so run it locally before you grade anyone with it.

// fake-upstream.mjs
import http from "node:http";

const state = { started: 0, completed: 0, aborted: 0 };

http.createServer((req, res) => {
  if (req.url === "/__stats") {
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify(state));
    return;
  }
  if (req.url !== "/v1/chat/completions") {
    res.writeHead(404).end();
    return;
  }

  state.started++;
  let finished = false;
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });

  let i = 0;
  const timer = setInterval(() => {
    if (i === 40) {                       // 40 chunks every 25 ms => ~1 s stream
      clearInterval(timer);
      finished = true;
      state.completed++;
      res.write("data: [DONE]\n\n");
      res.end();
      return;
    }
    res.write(`data: {"choices":[{"delta":{"content":"tok${i} "}}]}\n\n`);
    i++;
  }, 25);

  res.on("close", () => {                 // fires for a clean end and for a client abort
    if (finished) return;
    clearInterval(timer);
    state.aborted++;
  });
}).listen(8787, () => console.log("fake upstream on :8787"));
python
// proxy.mjs
import http from "node:http";

const UPSTREAM = process.env.UPSTREAM ?? "http://127.0.0.1:8787";
const IDLE_TIMEOUT_MS = 2000;
const MAX_RETRIES = 2;

const readBody = (req) => new Promise((resolve, reject) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => resolve(Buffer.concat(chunks)));
  req.on("error", reject);
});
const once = (emitter, event) => new Promise((r) => emitter.once(event, r));
const backoff = (n) => new Promise((r) => setTimeout(r, 50 * 2 ** n));

http.createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/chat") return void res.writeHead(404).end();

  const body = await readBody(req);
  const clientGone = new AbortController();
  // Bind the upstream lifetime to the response socket, not to the request body.
  res.on("close", () => { if (!res.writableEnded) clientGone.abort(new Error("client_gone")); });

  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    const upstream = new AbortController();
    const onClientGone = () => upstream.abort(new Error("client_gone"));
    clientGone.signal.addEventListener("abort", onClientGone, { once: true });

    const idle = setTimeout(() => upstream.abort(new Error("idle_timeout")), IDLE_TIMEOUT_MS);
    let firstByteSent = false;

    try {
      const up = await fetch(`${UPSTREAM}/v1/chat/completions`, {
        method: "POST",
        headers: { "content-type": "application/json", accept: "text/event-stream" },
        body,
        signal: upstream.signal,
      });

      if (up.status >= 500 && attempt < MAX_RETRIES) {
        clearTimeout(idle);
        await backoff(attempt);
        continue;
      }

      res.writeHead(up.status, {
        "content-type": up.headers.get("content-type") ?? "text/event-stream",
        "cache-control": "no-cache",
      });

      for await (const chunk of up.body) {
        idle.refresh();                                   // idle, not wall-clock
        firstByteSent = true;
        if (!res.write(chunk)) await once(res, "drain");   // respect backpressure
      }
      clearTimeout(idle);
      res.end();
      return;
    } catch (err) {
      clearTimeout(idle);
      if (firstByteSent || clientGone.signal.aborted) return void res.end();
      if (attempt === MAX_RETRIES) return void res.writeHead(502).end("upstream_unavailable");
      await backoff(attempt);
    } finally {
      clientGone.signal.removeEventListener("abort", onClientGone);
    }
  }
}).listen(3000, () => console.log("proxy on :3000"));

Two details in that file deserve explicit attention. The cancellation listener attaches to res, because req emits close when the request body finishes up and would abort every healthy stream. The retry guard checks firstByteSent, because a retry after the first chunk is a correctness bug rather than a resilience feature.

node fake-upstream.mjs & UP=$!
node proxy.mjs &        PX=$!
sleep 0.3

curl -sN -X POST localhost:3000/chat -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"hi"}]}' -o /dev/null
sleep 0.15

curl -sN -X POST localhost:3000/chat -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"hi"}]}' -o /dev/null --max-time 0.2 || true
sleep 0.15

curl -s localhost:8787/__stats
kill $UP $PX
# Criterion Weight Pass condition
1 Upstream cancelled on client disconnect 25 aborted increments within a few hundred milliseconds of the client abort
2 No retry after the first byte 20 no duplicated tok fragments appear in the client output
3 Backpressure respected 15 the proxy awaits drain instead of buffering the whole stream
4 Idle timeout, not a total timeout 10 a slow but live stream survives longer than the timeout window
5 Bounded retries before the first byte 10 repeated 5xx leads to at most two retries, then a 502
6 Deterministic test included 10 the suite passes with the fake upstream and no network access
7 Written trade-off note 10 the candidate names one real limitation of their own design

Criterion one carries the largest weight by design, and criterion seven breaks the tie between two submissions that both pass the harness. Neither is a style preference, since both map onto production incidents that a streaming service will eventually experience.

req.on("close") fires when the request body has been fully read, so every healthy stream looks abandoned and real users get dropped mid-answer.fetch promise keeps its connection open until the upstream finishes on its own schedule.res.write(). drain, a slow consumer causes unbounded buffering inside the process, which looks like a memory leak rather than a backpressure bug. The harness above is deliberately offline, but reviewers want a URL they can open and candidates need an endpoint that costs nothing to experiment against. The MonkeyCode project, which the operator of this account describes as open source, is presented as offering free model access and a free server option for exactly this kind of small deployment. The operator states an allowance on the order of ten million tokens together with a free server, so pointing UPSTREAM at that endpoint and running proxy.mjs on the free instance makes the demo reachable without a card on file.

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

Two honest caveats belong next to that recommendation. Free tiers change over time, so confirm the current allowance and terms on the project's own page instead of trusting a figure quoted in any blog post, including this one. A free allowance is also sized for harnesses and demos rather than load testing, so keep the fake upstream as the assertion target and treat the live endpoint as a smoke test.

Situation Fake upstream Free endpoint plus free server
Contract tests for cancellation and backpressure Required Optional
A demo URL a reviewer can open Not possible Useful
Comparing model output quality across weeks Not applicable Unsuitable, outputs drift
Sustained production traffic Not applicable Unsuitable
Measuring your own container's cold-start latency Not applicable Workable
An interview take-home that must cost nothing to grade Required Useful for the demo step

If you want a live endpoint behind the proxy during review, the free access described above covers a harness of this size, while the fake upstream remains the source of truth for pass and fail.

── more in #ai-infrastructure 4 stories · sorted by recency
── more on @openai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-disconnect-test-…] indexed:0 read:8min 2026-10-16 · —