DEV Community

Taylor Wang
Taylor Wang

Posted on

The Cache File Existed When I Deployed. Then the Free Server Recycled the Process.

I deployed a tiny job queue last week and watched it pass every check I threw at it. The health endpoint answered, the first jobs processed, and I closed the terminal with the kind of confidence that debugging usually punishes. Then the free server recycled the process, and my generated code forgot how to start.

The service was supposed to be boring: accept a job, store it, process it, and report the result. I generated the first version with a free model through MonkeyCode, and I deployed it to the free server option that comes with the same account, mostly because I wanted to see how far free infrastructure could carry a side project. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The model wrote clean, readable Python, and the server handed me a URL in seconds, so I did what everyone does after a green smoke test: I moved on.

The symptom that didn't look like a startup bug

About three hours after the deploy, the health check started failing, and the worst part was that it failed only sometimes. One request returned 200, the next returned 500, and the logs showed a stack trace that mentioned a file I had never touched by hand. I checked the database, the environment variables, and the request payloads, and none of them explained a failure that appeared and disappeared like a bad memory chip.

The trick that finally broke the case was checking how long the process had been alive. A quick ps call showed an elapsed time of seven seconds, which meant the service had restarted moments before I looked. The 500s were the windows where the process was still booting, and the 200s were the rare requests that landed after startup completed. I wasn't debugging a request handler at all; I was debugging a boot sequence that crashed in a loop.

Reproducing the failure in ten seconds

The stack trace pointed at pending_jobs.json, a file the generated code used as a tiny job store. The code opened it at startup with json.load(open(...)), and when the file was missing, the whole process died before it could serve a single request. Locally, the file always existed because my first run created it and my machine never deleted it. In my case, the free server recycled the process, and the file vanished with the old process. That's the whole story in one sentence.

It took me an embarrassingly long time to see that because I never simulated a restart with a clean filesystem. This one command reproduced the production failure instantly:

# simulate what the free server did to my process
rm -f pending_jobs.json
python app.py
# FileNotFoundError: [Errno 2] No such file or directory: 'pending_jobs.json'
Enter fullscreen mode Exit fullscreen mode

The reusable lesson is brutal and simple: if your generated code touches local state, delete that state and restart before you trust anything. A smoke test that runs once on a warm filesystem proves nothing about a cold boot, and free servers are very good at cold boots.

The root cause was an assumption, not a syntax error

The generated code was correct in every way that a linter can measure, and wrong in exactly the way that matters. It assumed the file it wrote would still be there tomorrow, and it had no code path for the moment when that assumption failed.

# generated: a job queue that stores pending jobs in a JSON file
import json
from pathlib import Path

CACHE = Path("pending_jobs.json")

def load_jobs():
    with open(CACHE) as f:          # crashes if the file is gone
        return json.load(f)

def save_jobs(jobs):
    with open(CACHE, "w") as f:
        json.dump(jobs, f)
Enter fullscreen mode Exit fullscreen mode

The fix is small, but it changes the failure mode from fatal to cosmetic. A missing file means an empty queue, and writes go to a temporary file that gets renamed into place so a crash mid-write can't corrupt the store.

def load_jobs():
    if not CACHE.exists():
        return []
    with open(CACHE) as f:
        return json.load(f)

def save_jobs(jobs):
    tmp = CACHE.with_suffix(".tmp")
    with open(tmp, "w") as f:
        json.dump(jobs, f)
    tmp.replace(CACHE)  # atomic on POSIX
Enter fullscreen mode Exit fullscreen mode

I also added a small script that simulates the recycle, so the next deploy can't hide this class of bug again. Start the app, write a job, kill the process, wipe the file, restart, and assert that the health check still passes.

#!/usr/bin/env bash
set -euo pipefail

python app.py &
APP_PID=$!
sleep 2
curl -fsS http://localhost:8000/health >/dev/null
echo "health check passed"

kill -TERM $APP_PID
wait $APP_PID || true
rm -f pending_jobs.json   # ephemeral storage wiped on recycle

python app.py             # must not crash on a cold boot
Enter fullscreen mode Exit fullscreen mode

The checklist I use before merging generated code

The real bug wasn't the missing file, and it wasn't even the model. It was my prompt, which described a queue but never described the environment where the queue would run. Now I audit every piece of state in generated code with a small decision table before I deploy.

State Lives where Survives restart? What to do
in-memory dict RAM no rebuild from a source of truth
local JSON file ephemeral disk maybe treat as disposable; default on missing
database row external DB yes keep, but handle connection timeouts
env var environment yes read at boot, fail fast with a clear message

Ask the generated code three questions before you merge it:

  1. Where does this state live — memory, local disk, a database, or an external service?
  2. What happens when the process restarts, and does the code have a default for every missing resource?
  3. What happens when two instances run at once, and would they corrupt each other's state?

If you can't answer any of them, you have found the next incident before your users do.

Who should not copy this fix

The atomic-write pattern makes a single-process service resilient, but it does not make it durable. If you lose jobs when the disk is wiped, if two instances write to the same file, or if your queue must survive a data-center failure, a local JSON file is the wrong tool no matter how elegantly you rename it. Use this approach for toys, prototypes, and internal tools where losing state is annoying but not catastrophic.

The funny part is that the same free model that wrote the fragile version wrote the fixed version in one follow-up prompt, once I told it the filesystem was ephemeral. The model wasn't the problem; my prompt was, because it described the feature and not the failure. What assumption has your generated code made about your environment lately?

Top comments (0)