DEV Community

Taylor Wang
Taylor Wang

Posted on

The Most Forgettable Scheduler I Ever Ran: 48 Hours With a Free Model and a Free Server

I love when software pretends to be stateless until it isn't. For 48 hours I ran a small scheduler on a free server, using a free model to classify Hacker News titles into backend, frontend, data, or devops buckets. The model did exactly what I asked, most of the time. The server did not share that reliability.

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

The experiment was simple on paper. Every ten minutes, a Python script would fetch the latest Hacker News stories, send each title to a free model endpoint, then store the category in a SQLite database. Once a day I wanted a report showing how many stories per bucket appeared in the last 24 hours. Nothing about this task is exotic. It is the kind of job you could implement with cron and a shell script, so it seemed like a fair first test for hosting a tiny AI pipeline on free infrastructure.

The first version ran without drama

Here is the core logic, stripped to its essentials:

# hn_classifier.py
import hashlib, re, sqlite3, time, json, urllib.request

DB = "/tmp/hn.db"
API = "https://hacker-news.firebaseio.com/v0"

def get_stories(limit=20):
    ids = json.load(urllib.request.urlopen(f"{API}/newstories.json"))[:limit]
    out = []
    for i in ids:
        item = json.load(urllib.request.urlopen(f"{API}/item/{i}.json"))
        if item and item.get("title"):
            out.append(item)
    return out

def call_free_model(prompt):
    # Thin wrapper around MonkeyCode's free model access.
    # The actual HTTP call is omitted because endpoint formats vary.
    raise NotImplementedError

def classify(title):
    prompt = f"Classify this title as backend, frontend, data, or devops. Reply with one word only. Title: {title}"
    raw = call_free_model(prompt)
    match = re.search(r"\b(backend|frontend|data|devops)\b", raw.lower())
    return match.group(1) if match else "unknown"

def save(title, category):
    key = hashlib.sha1(title.encode()).hexdigest()
    con = sqlite3.connect(DB)
    con.execute("INSERT OR IGNORE INTO stories(key, title, category, ts) VALUES(?,?,?,?)",
                (key, title, category, time.time()))
    con.commit()
    con.close()

def run():
    for story in get_stories():
        save(story["title"], classify(story["title"]))

if __name__ == "__main__":
    run()
Enter fullscreen mode Exit fullscreen mode

The script is intentionally small because the goal was to test the plumbing, not to build a product. For the first four hours the free model returned clean categories like backend and data, and the regex caught the one or two responses that included a period or a trailing comment. I was surprised by how easily a free tier handled what should have been a repetitive classification loop.

The restart erased more than memory

Then the free server restarted. I only noticed because my daily report came back empty, and after some digging I found that /tmp/hn.db had disappeared along with the process. The script still ran fine after the container came back, but every classification from the previous day was gone. This is where I made my first mistake: I tried to repopulate the database by re-fetching old stories. That failed because Hacker News rotates its story IDs quickly, and the old IDs had already fallen out of the newstories.json response.

The lesson was uncomfortable but clear: on that server, local disk is a suggestion, not a promise. If your workflow depends on a stateful database, a free server will eventually disappoint you.

Idempotency keys saved the next 24 hours

Instead of fighting the filesystem, I redesigned the job to be idempotent and append-only. Each story gets a key derived from a normalized version of its title, and the database uses INSERT OR IGNORE. If the file is wiped, the next run still processes the current batch and writes cleanly, no duplicate rows and no primary key violations.

def normalized_key(title):
    clean = re.sub(r"[^a-z0-9]+", "", title.lower())
    return hashlib.sha1(clean.encode()).hexdigest()
Enter fullscreen mode Exit fullscreen mode

This one change turned a flaky runtime into an annoyance instead of a data corruption event. I would repeat this idempotency-first design in any future free-server project, even before picking a logging strategy.

Cron was a lie, so I used a loop

My original setup included a cron expression, but after the first restart the job never came back. Free servers often do not expose a durable cron daemon, so I switched to a shell loop inside the container entrypoint. Not pretty, but effective:

#!/bin/sh
while true; do
  python3 hn_classifier.py
  sleep 600
done
Enter fullscreen mode Exit fullscreen mode

This loop restarts with the container, which means a reboot no longer kills the pipeline. The combination of an idempotent job and a restart-friendly entrypoint gave me the most stable 24 hours of the whole experiment.

Logs became the only persistent storage

Because the filesystem was unreliable, I redirected all classifier output to stdout and depended on the platform's log retention. Each run printed one JSON line with the timestamp, the title, and the assigned category. Those logs became my daily report. This works only if the platform keeps logs for long enough, so I would not ship this design without an external log sink in a real deployment.

What broke and what held

Layer What I tried What happened Would I repeat
Free model calls Short category prompts Mostly accurate, occasional verbose answers Yes, with output validation
Local SQLite /tmp/hn.db Wiped on restart Only if the volume is persistent
Cron Available, but unreliable Missed runs after reboot No, prefer a loop
stdout logs JSON lines Survived and provided the report Yes, external logging wins
Hacker News API Polling every 10 minutes Always available, but old IDs vanish Yes, use a snapshot

The table summarizes what I would tell myself before starting: model behavior was the least surprising part. The infrastructure was the wildcard.

Who should not use this setup

Do not use a free model plus a free server for anything that requires durable state, a strict SLO, or multi-step transactions. The combination shines for batch classification, filtering, formatting, and other stateless transforms. If you need at-least-once delivery, put the queue somewhere else and let the free layer be a stateless worker.

What I would repeat

  • Keep every job idempotent.
  • Send all output to stdout or an external sink.
  • Use a loop entrypoint so reboots restart the worker.
  • Validate the model's output before storing it.
  • Test a forced restart on day one, not day three.

The free model and free server from MonkeyCode are fine companions for a short-lived scheduled task. Just remember that “free” may mean “forgetful.” The fix is not to trust any local storage, but to design your workflow as if the server could vanish at any second. If you run a similar experiment, tell me which failure mode surprised you first. I already know mine.

Top comments (0)