DEV Community

Jordan Huang
Jordan Huang

Posted on

Same URL, Different Machine: Four State Myths on a Free Server

You deploy to a free server. The first request works. The second one forgets everything.

Is your code wrong? Was it a restart? Or is the URL just a label in front of a machine you never met?

I keep seeing the same three guesses in issue threads. All three are myths. Here is how I check, with a probe you can run in ten minutes.

The setup: one route, two questions

I want two answers from any free box. Which process am I talking to? And what state can that process honestly promise?

So I add a fingerprint route. It is boring on purpose. Stdlib only, no dependencies.

# app.py - stdlib only, no frameworks
import json, os, time, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

NONCE = uuid.uuid4().hex[:12]   # new value per process
COUNT = 0                        # a lie we are about to test

def boot_id():
    try:
        return open("/proc/sys/kernel/random/boot_id").read().strip()
    except OSError:
        return "unavailable"

def proc_start_ticks():
    # /proc/self/stat field 22 = starttime, in clock ticks since boot
    try:
        raw = open("/proc/self/stat").read().rsplit(") ", 1)[1].split()
        return int(raw[19])
    except (OSError, IndexError, ValueError):
        return -1

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        global COUNT
        COUNT += 1
        body = json.dumps({
            "nonce": NONCE,
            "pid": os.getpid(),
            "boot_id": boot_id(),
            "proc_start": proc_start_ticks(),
            "count": COUNT,
            "uptime_s": round(time.monotonic(), 1),
            "host": os.uname().nodename,
        }).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)

    def log_message(self, *a):
        pass

ThreadingHTTPServer(("0.0.0.0", 8080), H).serve_forever()
Enter fullscreen mode Exit fullscreen mode

Run it, expose port 8080, then poll it from your laptop:

# probe.py  ->  python probe.py https://your-app.example.com
import json, sys, time, urllib.request

url = sys.argv[1].rstrip("/") + "/"
prev = None
for i in range(12):
    with urllib.request.urlopen(url, timeout=10) as r:
        d = json.load(r)
    key = (d["nonce"], d["boot_id"], d["proc_start"])
    print(f"{i:02d} {'same' if key == prev else 'NEW':4s} "
          f"count={d['count']:3d} up={d['uptime_s']:7.1f}s "
          f"pid={d['pid']} host={d['host']}")
    prev = key
    time.sleep(5)
Enter fullscreen mode Exit fullscreen mode

That is the whole harness. Twelve requests, five seconds apart. Cheap enough that you will not fight a rate limit.

Myth 1: "One URL means one process."

This is the most expensive myth on the list. A URL is routing, not identity.

Look at the nonce. If it survives every poll, you have one process. If it changes, you are talking to someone new.

Now compare boot_id. Same nonce, same boot, same process. Different boot means a different kernel booted, so a different machine answered you.

Same boot but a new proc_start? Your process restarted on the same host. That is a crash, a redeploy, or an idle shutdown waking back up.

Illustrative output, not a measurement from your box:

00 same count=  1 up=    0.4s pid=41 host=box-a
01 same count=  2 up=   5.4s pid=41 host=box-a
02 NEW  count=  1 up=    0.4s pid=77 host=box-a
Enter fullscreen mode Exit fullscreen mode

That jump at index 02 is the whole story. Same host, new PID, counter back to one.

Myth 2: "In-memory state is cheap, so keep it."

In-memory state is cheap. It is also the first thing you lose.

Ask a sharper question. What does this counter actually guarantee? Nothing beyond the life of the process.

So I sort state by what breaks when it disappears:

  1. Liveness flags, caches, warm handles: fine in memory. Losing them costs latency, not truth.
  2. Job progress, retry budgets, "already emailed": never in memory. Losing them costs correctness.
  3. Counters a user paid for: never on the box at all. The box is not your ledger.

On a free server the suspend/resume behaviour is the thing to verify, not assume. I would rather measure it once than argue about it in a thread.

Myth 3: "A 200 with a body means the model finished."

This one hides inside free model access. You get a clean response, a tidy JSON body, and a stop reason you never read.

Then someone asks why the last paragraph is cut. The response was never complete. It was just delivered.

So I check the reason field on every call in the path I care about. Field names vary by provider, so read yours:

# shape varies - adapt, do not copy blindly
choice = resp.json()["choices"][0]
reason = choice.get("finish_reason")
if reason == "length":
    raise RuntimeError(f"truncated, not complete: {reason}")
Enter fullscreen mode Exit fullscreen mode

Now the failure is loud. A truncated answer becomes an exception, not a mystery.

This is the workflow I actually run: the fingerprint probe on a free server to see what my process is, and a free model call to check whether my completion is real. That pairing is why I use MonkeyCode's free model access and free server option, both operator-supplied availability rather than anything I can promise for you.

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

Myth 4: "If the write returned, the data landed."

A 200 on open(...).write() means the kernel accepted bytes. It does not mean the disk kept them past the next restart.

On ephemeral filesystems, files vanish with the instance. So I test the claim instead of trusting it. Write a marker, force a restart, poll again.

curl -s $URL >/dev/null; echo "marker-$(date +%s)" > /tmp/marker
# restart the instance from your dashboard, then:
curl -s $URL/marker   # 404 here means: treat /tmp as cache, not storage
Enter fullscreen mode Exit fullscreen mode

If that 404 happens, you learned something real in one restart instead of one incident.

The decision table I keep next to the probe

Signal you saw Likely cause Correct response
nonce changed, boot_id same Process restart or idle recycle Move durable state out of memory
boot_id changed Different host or fresh boot Never assume local disk persists
count resets between polls New process answered Check your retry and dedup keys
count jumps by more than your polls More than one instance served Read replicas cannot own a counter
finish_reason == "length" Output cap hit Treat as failure, retry with a plan

Five rows. That is the entire mental model. State belongs where instance identity does not matter.

Limitations, and who should skip this

This probe measures identity, not performance. It says nothing about latency, throughput, or cost.

Skip it if all four of these are true for you: paid always-on compute, one instance, a real database, and no idle suspend. You already have the guarantees you need.

The free-server route also is not for everything. If you need predictable cold start under a second, a suspend-on-idle box will fight you. If you need a signed uptime guarantee, read the terms before writing code around it.

One more warning. Do not loop the probe forever. Twelve requests is a test. Twelve thousand is a quota incident you caused yourself.

The corrected mental model

A URL is a promise to route bytes. It is not a promise about which process receives them.

A 200 is a delivery receipt. It is not a completion proof. A file write is a syscall. It is not a durability guarantee.

So fingerprint the process, read the stop reason, and move durable state somewhere that survives a reboot. Which of those three are you skipping right now?

Top comments (0)