Resume, Don't Restart: Overnight Batch Jobs on a Free Tier
A folder of 400 support emails sat on one laptop. A script summarised each email with a free model. At 03:12 the wifi blinked and the process exited. By morning there were zero results and the used quota was gone. That is the real free-tier problem. Model quality is rarely the blocker. The blocker is that a long job has no memory of what it finished. The repair is small. Write a journal entry after every item. On the next run, read the journal and skip the finished work. The job resumes instead of restarting, and the free allowance stops being something you pay for twice. The journal is the program A single append-only file carries all the state. Each line holds an item id, a status, and the result. Nothing else is needed to restart. JSONL beats a database here for one boring reason. An append survives kill -9 and a dead battery. You can also read it with tail while the job is still running. The runner below is about forty lines. It has four jobs: skip finished items, estimate cost before each call, record every outcome, and stop on command. # job.py - a resumable batch runner with a token budget import json, os, pathlib, sys, time RUN = pathlib.Path("run") RUN.mkdir(exist_ok=True) JOURNAL = RUN / "journal.jsonl" SPEND = RUN / "spend.json" KILL = RUN / "STOP" BUDGET = int(os.environ.get("TOKEN_BUDGET", "200000")) def record(item_id, status, **payload): row = {"id": item_id, "status": status, "at": time.time(), **payload} with JOURNAL.open("a") as f: f.write(json.dumps(row) + "\n") f.flush() os.fsync(f.fileno()) def finished(): if not JOURNAL.exists(): return set() lines = JOURNAL.read_text().splitlines() rows = [json.loads(line) for line in lines if line.strip()] return {r["id"] for r in rows if r["status"] == "ok"} def spent(): if not SPEND.exists(): return 0 return json.loads(SPEND.read_text())["tokens"] def bill(n): SPEND.write_text(json.dumps({"tokens": spent() + n})) def complete(prompt): """Return (text, tokens_used) from the client you already use.""" raise NotImplementedError def main(path): items = [json.loads(line) for line in open(path) if line.strip()] done = finished() for item in items: if KILL.exists(): print("kill switch found, stopping cleanly") break if item["id"] in done: continue estimate = len(item["prompt"]) // 4 if spent() + estimate > BUDGET: print("budget guard tripped before", item["id"]) break try: text, used = complete(item["prompt"]) except Exception as exc: record(item["id"], "error", error=str(exc)[:200]) continue bill(used) record(item["id"], "ok", text=text) print(item["id"], "ok", used) if __name__ == "__main__": main(sys.argv[1]) The complete function is deliberately empty. The transport does not matter to the runner. Swap in whichever client you already pay for, or do not pay for. A run you can reproduce in two minutes Build a tiny input file and a matching budget. Both are plain text, so the demo needs no setup. mkdir -p run printf '%s\n' '{"id":"m001","prompt":"Summarise: refund request, order 4471"}' > items.jsonl printf '%s\n' '{"id":"m002","prompt":"Summarise: login fails after password reset"}' >> items.jsonl TOKEN_BUDGET=2000 python job.py items.jsonl Run it once with a real client wired in. Kill the process halfway with Ctrl-C. Run the exact same command again. The finished ids are skipped and the run continues from the next unfinished line. Three commands cover daily operation. grep -c '"status": "ok"' run/journal.jsonl counts real progress. tail -f run/journal.jsonl shows the newest rows. touch run/STOP makes the loop exit at a safe boundary instead of mid-call. Where the free server fits The runner is a foreground process with one file of state. That means it fits almost anywhere. A laptop that stays awake is enough for a few hundred items. A long overnight queue needs a machine that does not sleep with the lid. MonkeyCode advertises free model access and a free server option for this kind of small job. At the time of writing, the promotion also states a free token allowance on the order of ten million tokens. Terms, capacity, and allowance can change, so check the current page before you depend on any number in a plan. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The honest fit is narrow. The free server is good for one sequential job that runs for hours. It is not a queue with workers, and it is not a contract with a response-time promise. Treat it as a place to park a script, not as infrastructure you promise to a customer. The decision table I actually use Situation Run it on Reason Under 200 items, one-off laptop no deploy step, journal sits beside the code Overnight queue, laptop sleeps free server option job survives a closed lid PII inside prompts self-hosted retention is your problem, not theirs Above a million tokens a day paid tier a free allowance is not a quota guarantee The table is the whole strategy. Most solo projects never leave row one, and that is fine. Limits worth stating out loud One item at a time is the design, so throughput is flat. A parallel version needs a claim step and a lock file, which roughly triples the code. The len // 4 estimate is a heuristic, not a meter. Bill from the usage number your client returns, and fall back to the estimate only when usage is missing. The journal grows without bound, so archive it per week. The spend file is not atomic, and two concurrent runs will corrupt it. Do not run this twice on the same directory. Errors are recorded and retried on the next run. That is intentional, and it also respects rate limits without a retry storm. A provider that returns 429 is telling you to stop, not to loop. Who should not use this Skip this pattern if prompts contain customer data you cannot legally ship. Skip it if you need sub-minute latency, because a batch job is the wrong shape. Skip it if a missed run costs money, since a free server is not a place for a payment crontab. Use it when a wrong answer is cheap and a rerun is free. That is the indie case: ship today, keep the bill at zero, accept the limits. The demo files above are enough to test the resume path this afternoon. If the promotion is still live when you read this, run the same script there and watch the journal grow instead of resetting.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to