跳到正文
DEV Community· Jordan Huang·· 3 小時前AI 評分59

免費伺服器作為 staging 的五個迷思與 90 秒探測流程

Cold Starts, Lost Files, and Green Dots: Five Myths About Using a Free Server as Staging

AI 導讀

文章指出,免費伺服器不應視為 staging 環境,並提供一套用 bash、curl 和 Python 標準庫測量冷啟動、檔案持久性及健康檢查的探測流程。流程以 /healthz 和 /canary 檢查冷熱請求時間及資料讀回,並將結果寫入 receipt.json;作者提醒,兩次測試只反映當晚狀況,不能代表整月穩定性。

正文

My demo broke at 9:58 PM. The same URL answered at 9:40. My code did not change in between.

That gap is the whole lesson. A free server is a shared, sleeping, replaceable machine. It is not a staging environment.

Below are five myths I kept repeating out loud. Then comes a 90-second probe that turns each one into a measurement.

Myth 1: "It returned 200, so it's up"

A 200 tells you one request finished. It says nothing about the next request.

Cold containers queue, boot, then answer. The status code hides all of that work.

So what did you actually measure? The status, or the latency budget?

Correct mental model: split the question into three.

  1. Liveness — the process exists.
  2. Readiness — it can serve a real request.
  3. Warmth — it can do that without a boot penalty.

Record connect time, time to first byte, and full body time separately. One number is not a diagnosis.

Myth 2: "My files will be there tomorrow"

Free tiers often rebuild the container between requests. Sometimes hourly. Sometimes weekly. You rarely get told which.

So don't assume persistence. Test it.

A canary is the cheapest test you have. Write a value, wait, read it back.

stamp=$(date +%s)
curl -s -X POST "$BASE/canary" -H 'Content-Type: application/json' -d "{\"v\":\"$stamp\"}"
sleep 120
curl -s "$BASE/canary?v=$stamp"

If the read-back is empty, your "temporary" upload directory is a story you tell your demo.

Correction: treat the filesystem as a cache. Keep state in an external store, or ship a seed script that rebuilds it on every boot.

Myth 3: "Free means I can retry CI forever"

Retries are cheap for you. They are not cheap for the host.

Many free tiers count requests, not successes. Worse, retry storms hit a cold service and trigger more boots.

Your pipeline gets slower. Your logs get noisier. Nothing gets fixed.

Correction: exponential backoff with jitter, a hard attempt cap, and one pre-warm request before the real one.

for i in 1 2 3; do
  code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$BASE/healthz")
  [ "$code" = "200" ] && break
  sleep $(( (2 ** i) + RANDOM % 3 ))
done

Myth 4: "Green locally means green remotely"

At least six things differ between your laptop and a free container:

  • Timezone — logs land on the wrong day.
  • Clock — token expiry and signed URLs drift.
  • Env vars — one missing key kills a code path you never exercised.
  • Filesystem — sometimes read-only, sometimes wiped.
  • Memory ceiling — your build is fine until it isn't.
  • Outbound network — allowlists and DNS behave differently.

None of these appear in a unit test. All of them appear in a demo.

Myth 5: "The health check proves demo readiness"

Most health endpoints are shallow. They return 200 while the connection pool is still warming.

A green dot means the route exists. It does not mean the first real user succeeds.

Correction: add one endpoint that touches a real dependency. Hit it twice before you present.

The probe: one script, one receipt

Here is the template I use now. It needs only bash, curl, and the Python standard library.

You supply two routes: /healthz and /canary. The second one stores and returns a small value.

#!/usr/bin/env python3
# probe.py <base_url> [sleep_seconds] — stdlib only, writes receipt.json
import datetime, json, sys, time, urllib.request

BASE = sys.argv[1].rstrip("/")
WAIT = float(sys.argv[2]) if len(sys.argv) > 2 else 0.0

def timed(path, payload=None, timeout=25):
    req = urllib.request.Request(BASE + path, method="POST" if payload else "GET")
    if payload:
        req.add_header("Content-Type", "application/json")
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(req, data=payload, timeout=timeout) as r:
            body = r.read().decode("utf-8", "replace")
            return {"path": path, "status": r.status,
                    "ms": round((time.perf_counter() - t0) * 1000, 1),
                    "body": body[:200]}
    except Exception as e:
        return {"path": path, "status": None,
                "ms": round((time.perf_counter() - t0) * 1000, 1),
                "error": str(e)[:200]}

stamp = str(int(time.time()))
checks = [
    timed("/healthz"),                                   # possibly cold
    timed("/healthz"),                                   # hopefully warm
    timed("/canary", payload=json.dumps({"v": stamp}).encode()),
]
time.sleep(WAIT)
checks.append(timed("/canary?v=" + stamp))               # canary read-back

receipt = {
    "base": BASE,
    "at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "wait_seconds": WAIT,
    "checks": checks,
}
json.dump(receipt, open("receipt.json", "w"), indent=2)
print(json.dumps(receipt, indent=2))

Run it twice. Once cold, once after a short pause.

python3 probe.py https://your-app.example 0 > cold.json
python3 probe.py https://your-app.example 120 > warm.json

Reading the receipt

Do not eyeball the whole file. Check four fields.

Field Healthy sign What a bad value means
healthz[0].ms vs healthz[1].ms Second call much faster Cold-start tax is real; pre-warm before demos
canary status 200 with the stored value Your endpoint is broken, not the host
read-back body Contains your stamp State did not survive; treat FS as cache
any error Empty Timeout or TLS issue; fix before blaming the app

Two runs prove very little about a month. Two runs prove a lot about tonight's demo.

Where a free model fits (and where it does not)

I use a model to summarize the receipt into three sentences for a shared channel. That is it.

A summary is not a gate. Never let a generated paragraph decide whether you ship.

For that write-up step I have used MonkeyCode's free model access and its free server option, so the probe and the summary live in one place. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Run the probe yourself regardless of host. The numbers are yours, not mine.

Decision table: is a free server enough?

Situation Free server OK? What to do instead
Internal demo, one audience, you can redeploy Yes Pre-warm, keep a seed script ready
Overnight CI on every commit Risky Cache artifacts, cap retries at 3
Data you cannot rebuild No External database with backups
Latency-sensitive user tests No Paid tier or local container
Load or soak testing No Isolated environment you control
Teaching a workflow, first pass Yes Keep the probe in the repo

Limitations and who should not use this

The probe measures only what you instrument. It cannot see the host's scheduler, quotas, or eviction policy.

It also cannot promise anything about next month. Free tiers change shape without notice.

Skip this approach entirely if you need an SLA, handle regulated data, or run stateful services. Skip it if a mid-demo redeploy would cost you the deal.

Also skip it if you cannot write the canary endpoint. Without it, you are guessing.

What I changed

I stopped calling the free box "staging". I call it a demo target with a timer.

The receipt file now lives in the repo. Every demo starts with two runs and one glance at the table.

What would your 9:58 PM failure look like, if you measured it at 9:40?

來源:DEV Community · dev.to