DEV Community

Dakota Wu
Dakota Wu

Posted on

A Cold-Start Ledger for Shipping One Indie Route on a Free Server

A solo API earns a free-server deploy only when process startup does no network I/O, starts no thread, and writes nothing that must survive the process. That rule is the shipping decision. A warm laptop hides violations, because the process never sleeps and the disk never disappears. This ledger fails the ship on the founder's machine before a free host or a free-model retry pays for the mistake.

The check is local and cheap. It reads Python files, walks the syntax tree, and returns a non-zero status when module-level code calls a network client, starts a worker, or opens a path outside an allowed temp prefix. The script in this article is an unexecuted example. The founder should run it, read every hit, and keep judgment over lines the scanner cannot see.

Where the free path actually breaks

Indie drafts often import a client and call it at the top of the file so a sample looks ready. The first request on a laptop is fast, because that import already happened an hour ago. A free server that sleeps must import again on wake, and the import now sits inside the visitor's timeout.

Disk is the second break. A SQLite file beside the source tree looks like a zero-cost database until the platform discards the filesystem. The repair loop is the third break. A timed-out route sends the founder back to a coding assistant, and the easiest patch is a paid broker, a hosted queue, or a box that never sleeps.

None of those patches belong in a ship-today plan. The ledger exists so the route stays small enough that a free path is still an honest choice.

1. Freeze the budget in a file

Policy stays in coldstart-budget.json at the project root. The values are local choices for this app, not a vendor quota and not a measured latency.

{
  "scan_roots": ["app"],
  "forbidden_import_roots": ["requests", "httpx", "socket", "urllib", "boto3", "stripe"],
  "forbidden_suffixes": ["Thread", "Popen", "create_connection"],
  "allowed_write_prefixes": ["/tmp/", "./tmp/"]
}
Enter fullscreen mode Exit fullscreen mode

A listed root may be imported. Calling that root while the module loads is the defect the ledger rejects. Importing requests is ordinary, while calling requests.get at import time is not. The scan root stays on app, not on a virtualenv, or third-party packages will flood the report.

2. Keep one bad file and one good file

The bad module calls the network while Python is still loading it. A sleeping host repeats that call on every wake.

# app/bad_health.py — should fail the ledger
import requests

READY = requests.get("https://example.invalid/health", timeout=2).status_code
Enter fullscreen mode Exit fullscreen mode

The good module binds a function and stops. Work starts only after a request handler calls that function.

# app/health.py — should pass the ledger
import requests

def health():
    response = requests.get("https://example.invalid/health", timeout=2)
    return response.status_code
Enter fullscreen mode Exit fullscreen mode

example.invalid is a reserved documentation name, not a live host. The samples exist so the scanner has one file to reject and one file to accept. They are not a production probe, and the founder should not point them at a customer domain.

3. Scan import-time calls

Save the following as coldstart_ledger.py. It uses the Python standard library only, so a fresh laptop can run it without a package install. Treat it as an unexecuted example until the founder has run it on a copy of the tree.

#!/usr/bin/env python3
"""Unexecuted example: flag import-time network, thread, and unsafe file writes."""

import ast
import json
import sys
from pathlib import Path

def dotted(node):
    if isinstance(node, ast.Name):
        return node.id
    if isinstance(node, ast.Attribute):
        head = dotted(node.value)
        return f"{head}.{node.attr}" if head else node.attr
    return ""

def inside_function(stack):
    kinds = (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)
    return any(isinstance(item, kinds) for item in stack)

class Ledger(ast.NodeVisitor):
    def __init__(self, path, budget, findings):
        self.path = path
        self.budget = budget
        self.findings = findings
        self.stack = []

    def generic_visit(self, node):
        self.stack.append(node)
        super().generic_visit(node)
        self.stack.pop()

    def visit_Call(self, node):
        if not inside_function(self.stack):
            name = dotted(node.func)
            root = name.split(".")[0]
            suffix = name.split(".")[-1]
            blocked_root = root in self.budget["forbidden_import_roots"]
            blocked_suffix = suffix in self.budget["forbidden_suffixes"]
            if blocked_root or blocked_suffix:
                line = f"{self.path}:{node.lineno}: import-time call {name}"
                self.findings.append(line)
            self._check_open(node, suffix)
        self.generic_visit(node)

    def _check_open(self, node, suffix):
        if suffix != "open" or not node.args:
            return
        arg = node.args[0]
        if not isinstance(arg, ast.Constant) or not isinstance(arg.value, str):
            self.findings.append(f"{self.path}:{node.lineno}: non-literal open() at import")
            return
        prefixes = tuple(self.budget["allowed_write_prefixes"])
        if not arg.value.startswith(prefixes):
            self.findings.append(f"{self.path}:{node.lineno}: write outside temp ({arg.value})")

def main():
    budget_path = Path("coldstart-budget.json")
    budget = json.loads(budget_path.read_text(encoding="utf-8"))
    findings = []
    for root in budget["scan_roots"]:
        base = Path(root)
        if not base.exists():
            findings.append(f"missing scan root: {root}")
            continue
        for path in base.rglob("*.py"):
            source = path.read_text(encoding="utf-8")
            tree = ast.parse(source, filename=str(path))
            Ledger(path, budget, findings).visit(tree)
    if findings:
        print("\n".join(findings))
        return 1
    print("cold-start ledger: no import-time calls flagged")
    return 0

if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Function bodies are ignored on purpose. A call inside health is request work, and request work is allowed. A call in the module body, including a call hiding in __init__.py, is startup work. Startup work is what a cold free server repeats for every wake.

4. Run the ledger before any host is chosen

The ledger must run as its own process. Importing the app inside the same interpreter would pay the cold-start cost the check is meant to catch. From the project root, with app/bad_health.py still present, the expected result is a failure.

python3 -m py_compile coldstart_ledger.py
python3 coldstart_ledger.py
echo "ledger_exit=$?"
Enter fullscreen mode Exit fullscreen mode

A failing run prints a path, a line number, and the call name, then exits 1. Moving the call into a function, or removing bad_health.py from app/, should print cold-start ledger: no import-time calls flagged and exit 0. If the status stays 1, the founder reads the printed line instead of guessing. A syntax error aborts with a traceback, so that file gets fixed locally and the ledger runs again before any deploy command exists.

5. Spend free model access on one flagged file

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

MonkeyCode fits this workflow in two optional places, and nowhere else. Free model access can rewrite a single flagged file so the network call moves inside a handler and keeps a timeout. The free server option is a destination for the route after the ledger exits 0, not a reason to skip the ledger. This draft treats those two availability claims as operator-supplied and current. It names no model, no token allowance, no machine size, and no duration, because those facts move and a post should not freeze them.

The same-day read is part of the method. Before either free option is used, the founder opens the current plan notes and checks that model access is still free, that a no-charge server plan is still offered, and that sleep, disk, and request limits still match this route. If the notes disagree with this article, the notes win.

The assistant prompt stays narrow. It asks for the import-time call to move into a function, for a timeout on the outbound request, and for no new host, queue, or database. After the edit, the ledger runs again. A green scan plus a new paid hostname is still a failed ship. The scanner does not understand pricing. The founder does.

6. Map the exit status to a ship decision

The table is the deploy policy for the day. It assumes nothing about a vendor's future pricing.

Ledger result Same-day action
Exit 0, and the diff adds no external host Deploy that one route and watch the first cold request
Exit 1, with one import-time call Rewrite that call, re-run, then decide
Exit 0, but the route needs a worker or durable disk Keep the route off the free server
Assistant patch adds a broker or SMTP host Reject the patch and stay on the laptop
Scanner raises SyntaxError Fix the file locally; do not deploy around the crash

The first cold request remains a manual check. The ledger cannot see the platform's real wake time. The founder calls the route once after an idle period, confirms a response, and only then shares the URL.

Limits, and who should leave the free path

The scanner is a syntax screen, not a proof of safety. It misses exec, string-built imports, and calls assembled with getattr. A non-literal open() is reported as review work, not classified as safe. A green run is not a load test, not a secret scan, and not evidence that a free tier will still exist next month.

Some products should not use this approach at all. A websocket that must stay connected, a billing job that must run while nobody is browsing, and any personal-data store that lives only on local disk do not fit a host that may sleep or wipe files. A team that needs a stated uptime commitment needs a plan that actually states one. This ledger will not create that commitment.

Another assisted rewrite will not repair those mismatches. Spending a free model turn to paper over a sleeping host burns the allowance and leaves the route wrong. The honest outcome is a smaller route, or a paid host chosen in the open.

Close the loop on one route

The founder selects one read-only route, runs the ledger, and ships that route only when startup stays quiet. Every other route waits. That restraint is what keeps today's bill at zero.

A founder who wants help with the rewrite can point MonkeyCode's free model access at the single flagged file, re-run the ledger, and confirm the free-server notes before any deploy command. The ledger, not the assistant, decides whether the route may leave the laptop.

Top comments (0)