Technology Sep 03, 2026 · 10 min read

The Health Check Said 200. ss Showed a Different Interpreter.

Have you ever celebrated a green health check and then noticed nobody could actually use the app? I spent a messy forty-eight hours on that exact feeling, and the logs never once contradicted me. The checker kept hitting something that answered politely, so I kept blaming the client instead. The pro...

DE
DEV Community
by Taylor Wang
The Health Check Said 200. ss Showed a Different Interpreter.

Have you ever celebrated a green health check and then noticed nobody could actually use the app? I spent a messy forty-eight hours on that exact feeling, and the logs never once contradicted me. The checker kept hitting something that answered politely, so I kept blaming the client instead. The process I actually cared about was never the one holding that soothing open port.

This is not a manifesto about microservices, meshes, or the correct way to ship health. It is a set of field notes on a tautological smoke test, plus a receipt I wish I had run on hour one. If you generate servers with a coding model and then run them on a laptop and a remote box, this trap is waiting.

Why did a 200 feel like a lie?

I asked a free coding model for the smallest possible smoke test around a tiny HTTP app. The draft looked responsible: start the server, sleep one beat, request the path, assert 200, and print OK. What could possibly go wrong with four lines that every tutorial keeps repeating without shame?

The first problem was not the framework, the router, or some fashionable async runtime hiding under the floor. The problem was the word localhost, which feels like a constant until you actually change machines. My laptop still had an old listener on the same port from a previous experiment I had forgotten to kill. The generated checker never asked who answered the socket. It only asked whether somebody, anybody, answered it.

Have you looked at ss on the same heartbeat as your client, or do you trust the status line? I trusted the status line, because 200 is a very soothing number when you are tired. Soothing numbers are how you donate a whole day to the wrong process.

What I tried while the port still answered

I did the usual local dance first, because that is what tired people do when a green check appears.

  1. Restart the app and rerun the checker until both look green in the same terminal scrollback.
  2. Print the port in the server logs and in the checker logs, then declare the numbers matched.
  3. Switch from curl to urllib so the test supposedly belongs to Python instead of a shell.
  4. Blame the remote environment when the same script felt flaky after I copied the files over.

None of those steps asked a process identity question, which is the only question that mattered. They asked a port question, and ports are shared, recycled, and inherited by whatever started first. Python's stdlib HTTPServer also sets address reuse, so a successful bind is not exclusive ownership of that tuple.

Here is the shape of the generated checker, reconstructed as a lab example rather than a transcript of a private chat:

# lab example — not a recorded transcript
import urllib.request

url = "http://localhost:8000/health"
with urllib.request.urlopen(url, timeout=2) as response:
    assert response.status == 200
print("ok")

Looks harmless, right? It is harmless until two interpreters exist on the same port story. Then it becomes a coin flip with extra logging and a very confident exit code.

The break: two listeners, one hostname

The break showed up when I finally printed sys.executable and os.getpid() from the app, and then from a response header the app controlled. The checker was still happy, because happiness was defined as status 200. The header was not the nonce I had just started in this shell. I had been scoring a previous process that never died, and it was still doing customer-service impressions.

On the laptop, localhost meant my leftover listener, which had survived a terminal I thought I had closed. On the remote box, localhost meant a different network namespace than the one I was calling from my own machine. Two failures, one slogan, and the slogan was "the service is up." Would you have killed the old process first, or would you have trusted the client too?

I used MonkeyCode for the pairing that finally made the contradiction visible: free model access to draft the checker, and a free server option so the laptop could not keep answering the socket. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am not claiming a particular model name, a quota, a GPU SKU, or a forever-free promise. I am claiming this workflow only: generate on one side, then execute on a machine that is not your leftover port.

A receipt you can rerun

The artifact is a pair of stdlib scripts you can read in one sitting. One server stamps every response with a nonce you choose at boot. One checker refuses to accept 200 unless that nonce, the path, and the explicit host all match.

Label: this is a lab receipt, not a production health system, and not a claim that I published a benchmark.

The server that names itself

# nonce_server.py — lab example
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
import sys

NONCE = os.environ["APP_NONCE"]
HOST = os.environ.get("BIND_HOST", "127.0.0.1")
PORT = int(os.environ.get("BIND_PORT", "8000"))

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/health":
            self.send_error(404)
            return
        body = b"ok\n"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("X-App-Nonce", NONCE)
        self.send_header("X-App-Pid", str(os.getpid()))
        self.send_header("X-App-Executable", sys.executable)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        sys.stderr.write("server pid=%s exe=%s " % (os.getpid(), sys.executable))
        sys.stderr.write((fmt % args) + "\n")

if __name__ == "__main__":
    httpd = ThreadingHTTPServer((HOST, PORT), Handler)
    print(
        "listening",
        httpd.server_address,
        "pid",
        os.getpid(),
        "exe",
        sys.executable,
        "nonce",
        NONCE,
        flush=True,
    )
    httpd.serve_forever()

The checker that can fail on purpose

# smoke_check.py — lab example
import argparse
import sys
import urllib.error
import urllib.request

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", required=True)
    parser.add_argument("--port", type=int, required=True)
    parser.add_argument("--expect-nonce", required=True)
    parser.add_argument("--path", default="/health")
    args = parser.parse_args()
    url = "http://%s:%s%s" % (args.host, args.port, args.path)
    try:
        with urllib.request.urlopen(url, timeout=2) as response:
            nonce = response.headers.get("X-App-Nonce", "")
            pid = response.headers.get("X-App-Pid", "")
            exe = response.headers.get("X-App-Executable", "")
            status = response.status
            body = response.read(64)
    except urllib.error.URLError as exc:
        print("UNREACHABLE", url, exc)
        return 2
    print("status", status, "nonce", nonce, "pid", pid, "exe", exe, "body", body)
    if status != 200:
        print("BAD_STATUS")
        return 3
    if nonce != args.expect_nonce:
        print("WRONG_PROCESS expected", args.expect_nonce)
        return 4
    print("MATCH")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Commands I would type in order

export APP_NONCE="run-$(date +%s)"
export BIND_HOST=127.0.0.1
export BIND_PORT=8000
python3 nonce_server.py

# other terminal, same machine first
python3 smoke_check.py --host 127.0.0.1 --port 8000 --expect-nonce "$APP_NONCE"

Then copy the same two files to the remote box and export a fresh nonce there. Do not reuse the laptop nonce, because reuse turns the header back into theater. Do not let the checker default the host, even if a model offers a shorter command. If the remote checker needs 127.0.0.1, say that out loud in the argv. If you are calling from your laptop toward the remote process, you need the remote bind address that is actually reachable, not the word localhost from a model reply.

A second snapshot I now take before I believe anyone, including myself:

python3 - <<'PY'
import os, sys
print("pid", os.getpid())
print("exe", sys.executable)
print("cwd", os.getcwd())
PY
ss -ltnp 2>/dev/null || netstat -ltn

ss is the adult in the room. The HTTP client is the intern with a green sticker and no memory of yesterday's process.

Decision table: who answered you?

Checker runs on --host value Server BIND_HOST Nonce matches? What it actually proved
Laptop localhost leftover local process no You have a listener, not this listener
Laptop 127.0.0.1 remote server only unreachable Laptop loopback is not the remote job
Remote 127.0.0.1 127.0.0.1 on remote yes Same-machine smoke, still not public reachability
Remote 0.0.0.0 as connect host 0.0.0.0 bind confusing 0.0.0.0 is a bind trick, not a connect target
Laptop remote hostname 127.0.0.1 on remote unreachable Bound to loopback, hidden from you
Laptop remote hostname reachable interface yes This is the first row that means clients can call it

Read the last two rows twice before you change a bind address in anger. Binding to loopback on a free server will make a remote checker fail even when the process is healthy. Binding to every interface will make a checker pass and will also make neighbors interesting, which is a different incident with a worse postmortem.

I do not bind to 0.0.0.0 on a shared box just to turn a table cell green. If you need a remote check, bind a specific interface you understand, or run the checker on the same machine. Treat public reachability as its own row, not as a prize for a louder listen socket.

What broke after the nonce existed

Even with the header in place, I still wasted hours on three smaller lies that impersonate a dead service.

  • Address family: localhost sometimes means ::1 while the app bound 127.0.0.1. The error looks like a dead process. It is a family mismatch with excellent comic timing.
  • Stale environment: I exported APP_NONCE in one shell and started the server in another shell. The checker was right. I was not, and I argued with it anyway.
  • Buffered banners: the server printed listening without flush=True, and I started the checker before the bind finished. Timing is not identity, but it impersonates identity extremely well.

The IPv6 row is the meanest, because a coding model will happily emit localhost forever and never mention getaddrinfo. Force 127.0.0.1 or force ::1 in both the server and the checker. Do not let a name pick an address family for you during a fire.

What I would repeat

I would repeat the nonce header, the required --host flag, and the ss snapshot before any celebration. I would refuse any checker that hardcodes localhost, even when the generated comment says the code is production-ready. I would run the checker on the same machine as the app before I run it across the network. I would print sys.executable, because two Pythons on one box is not a thought experiment.

I would not repeat trusting a generated snippet that asserts 200 and exits zero. I would not repeat reusing ports across experiments because the operating system is more patient than I am. I would not repeat treating "it works on my laptop" as evidence about a remote listener that I have never identified.

Limitations, and who should not use this

This receipt does not prove correctness, latency, TLS, authentication, authorization, or load. It proves that the HTTP process you think you started is the one that answered one GET. That is a low bar, and it still failed me for two days, which is the whole point of writing the notes down.

Do not put secrets, customer data, or production traffic on a shared free server, including a box you are only "borrowing for a demo." Do not treat a free model as a source of network truth, because it will optimize for a short command. Do not use this pattern if you already have a mesh, a workload identity system, or a platform health check you actually trust. Do not copy the server onto a public bind just to make the decision table turn green.

If your incident is DNS, certificates, HTTP/2, or a reverse proxy stripping unknown headers, this stdlib toy will waste your time. If your incident is "wrong process, right port," it will save the second day. I am still a little angry at localhost. Are you?

If you run the table, tell me which row you landed on first. I am collecting those rows more carefully than I collect status codes.

DE
Source

This article was originally published by DEV Community and written by Taylor Wang.

Read original article on DEV Community
Back to Discover

Reading List