DEV Community

Finley Zhou
Finley Zhou

Posted on

A Free Model's Server Passed My Smoke Test, Then Hung on Shutdown. The Exit Contract Caught It.

We spend a lot of time testing whether a service can start. We spend far less time testing whether it can exit. A free model can scaffold an HTTP server, and a free server can host it, but if the process cannot stop cleanly, cheap compute becomes expensive: stuck containers, half-open sockets, and failed restarts.

For this experiment I used MonkeyCode's free model access to generate a minimal Python server, and I planned to run it on the free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I did not compare model quality or quote benchmarks; the only thing I tested was observable process behavior.

The generated server looked fine

The first version looked like a normal smoke-test pass:

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json, signal, time

class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_GET(self):
        if self.path == "/health":
            body = json.dumps({"ok": True}).encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        elif self.path == "/slow":
            time.sleep(30)
            body = json.dumps({"ok": True, "slow": True}).encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_error(404)

if __name__ == "__main__":
    server = ThreadingHTTPServer(("0.0.0.0", 8080), Handler)

    def stop(signum, frame):
        server.shutdown()

    signal.signal(signal.SIGTERM, stop)
    server.serve_forever()
Enter fullscreen mode Exit fullscreen mode

GET /health returned 200 quickly. A quick look showed a threaded server, a signal handler, and a shutdown path. That is normally enough to move on.

The problem is subtle and only appears when a request is in flight during shutdown. ThreadingHTTPServer does not use daemon threads by default. When SIGTERM arrives, the signal handler returns from serve_forever(), but a non-daemon handler thread continues to execute. The process does not actually exit until that thread finishes—here, thirty seconds later.

A free server that cycles instances or sends a restart while one slow request is running can then sit in a zombie-like state: it is no longer accepting work, but it is still holding resources.

The test that should have run earlier

A smoke test only hits /health. I added an exit contract test that starts the process, opens a slow request, sends SIGTERM, and then requires the process to disappear within a generous but finite window:

python server.py &
pid=$!

cleanup() {
  kill -9 "$pid" 2>/dev/null
  kill -9 "$slow_pid" 2>/dev/null
}
trap cleanup EXIT

for _ in $(seq 1 50); do
  curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1 && break
  sleep 0.1
done

curl -sS http://127.0.0.1:8080/slow >/dev/null 2>&1 &
slow_pid=$!
sleep 0.2

kill -TERM "$pid"
for _ in $(seq 1 20); do
  if ! kill -0 "$pid" 2>/dev/null; then
    echo "server exited cleanly"
    trap - EXIT
    exit 0
  fi
  sleep 0.1
done

echo "server hung on shutdown" >&2
exit 1
Enter fullscreen mode Exit fullscreen mode

On the generated version, this test fails: the main loop returns, but the slow thread keeps the process alive. The problem is not the free model being "dumb." It generated the code anyone might write on a first pass. The missing piece is that shutdown is a contract, not an implementation detail.

The minimal fix

Set the thread policy before starting the server:

server = ThreadingHTTPServer(("0.0.0.0", 8080), Handler)
server.daemon_threads = True
Enter fullscreen mode Exit fullscreen mode

With that line, the slow request is running in a daemon thread. The process can exit after serve_forever() returns, and the exit contract test passes. I then ran the same test again before considering the free server canary.

This is a small fix, but the important artifact is the test, not the one-line patch. The test defines acceptance criteria that a generated server must meet before it gets a public route, even a transient free one.

Why this matters on free server infrastructure

Free server options are useful for canaries, demos, and small experiments. They are also more likely to be idled, restarted, or stopped when they are underused. If a service cannot exit cleanly, a restart can become a pause, a health check can race with a half-dead process, and a developer ends up debugging infrastructure instead of code.

The workflow that worked for me was:

  1. Write the observable contract first: a /health response and a required exit window.
  2. Generate the smallest server possible, not a framework showcase.
  3. Run the exit test locally before deploying anywhere.
  4. Repeat the test against the free server after deployment if the environment permits sending signals or restarting the instance.
  5. Treat the generated code as a canary, not as a production service.

Limitations

This test says almost nothing about model quality. It does not check security, data correctness, response latency under load, prompt injection resilience, token consumption, or whether the free server will remain available. It also assumes you can start and signal a local process; it will not catch bugs that only occur inside a specific container runtime.

It is also not a substitute for reviewing the generated diff. A service can exit cleanly and still be dangerous. The exit contract is one gate among many.

Who should not use this approach

Do not use a generated, free-tier server for production payment data, personal health information, secrets, or any workload that requires durable storage and guaranteed uptime. If your team is not willing to read the generated code or maintain a small test harness, the free tier becomes a liability rather than a shortcut.

The useful conclusion is not that a free model can write a server. It is that cheap generation lowers the cost of producing candidates, while the cost of verifying their behavior stays with you. If you try MonkeyCode's free model and server for a similar canary, put the contract file in the repository. The prompt is not the artifact; the exit test is.

Top comments (0)