A solo founder sat at a kitchen table on Saturday. The landing page already accepted a first email. One reminder still blocked a public launch.
A coding agent offered a patch in seconds. It imported Redis, Celery, and a worker image. The Saturday ship died before the first bill.
A kitchen timer does not hire a dispatcher. It sits on the kitchen counter and ticks. Early product code can tick the same way.
Cloud queues shine when many machines share work. A first version still has only one process. Redis is a freight truck assigned to a paper route.
The smaller design is enough for launch. Time becomes an injected value instead. Delayed work becomes ordinary SQLite rows.
Handlers that call datetime.now hide the clock. Tests cannot move that hidden wall clock. The agent then invents cron and extra workers.
A tiny clock object keeps time visible. Production code passes the real wall clock. Tests pass a frozen kitchen clock instead.
The harness below is a proposed local kit. It is meant for a single-process reminder loop. Paste it, then run pytest on the test file.
# kitchen_clock.py
from datetime import datetime, timedelta, timezone
class KitchenClock:
def __init__(self, now=None):
self._now = now or datetime.now(timezone.utc)
def now(self):
return self._now
def advance(self, seconds: int) -> None:
self._now = self._now + timedelta(seconds=seconds)
The clock class is boring on purpose. Boring clocks do not page operators at night. A Saturday launch needs that kind of silence.
A reminder is not a queue message at all. It is a row with a due timestamp. SQLite stores that row beside the application code.
# job_table.py
import json
import sqlite3
from pathlib import Path
SCHEMA = """
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
payload TEXT NOT NULL,
run_at TEXT NOT NULL,
done_at TEXT
);
"""
class JobTable:
def __init__(self, path: Path):
self.conn = sqlite3.connect(path)
self.conn.execute(SCHEMA)
self.conn.commit()
def enqueue(self, kind, payload, run_at):
self.conn.execute(
"INSERT INTO jobs (kind, payload, run_at) VALUES (?, ?, ?)",
(kind, json.dumps(payload), run_at.isoformat()),
)
self.conn.commit()
def due(self, now):
return self.conn.execute(
"""
SELECT id, kind, payload
FROM jobs
WHERE done_at IS NULL AND run_at <= ?
ORDER BY id
""",
(now.isoformat(),),
).fetchall()
def mark_done(self, job_id, now):
self.conn.execute(
"UPDATE jobs SET done_at = ? WHERE id = ?",
(now.isoformat(), job_id),
)
self.conn.commit()
The table does not retry work forever. It does not promise exactly-once delivery either. A solo launch can accept those public limits.
Handlers receive the clock as a real argument. They never read the machine clock directly. The runner marks a row done after success.
# runner.py
import json
class JobRunner:
def __init__(self, clock, table, handlers):
self.clock = clock
self.table = table
self.handlers = handlers
def tick(self):
now = self.clock.now()
for job_id, kind, payload in self.table.due(now):
handler = self.handlers[kind]
handler(json.loads(payload), self.clock)
self.table.mark_done(job_id, now)
Mail leaves through a port, not a global. Tests replace that port with a list. Production later wires a real sender object.
# reminders.py
def make_reminder_handler(mailer):
def handle_reminder(payload, clock):
mailer.send({
"to": payload["email"],
"subject": "Your trial ends soon",
"sent_at": clock.now().isoformat(),
})
return handle_reminder
The proof is a jump in fake time. No process sleeps through a fake day. No Redis container has to boot first.
# test_reminders.py
from datetime import datetime, timedelta, timezone
from kitchen_clock import KitchenClock
from job_table import JobTable
from reminders import make_reminder_handler
from runner import JobRunner
def test_reminder_fires_after_one_day(tmp_path):
start = datetime(2026, 9, 7, 9, 0, tzinfo=timezone.utc)
clock = KitchenClock(now=start)
table = JobTable(tmp_path / "jobs.sqlite")
sent = []
class ListMailer:
def send(self, msg):
sent.append(msg)
handler = make_reminder_handler(ListMailer())
runner = JobRunner(clock, table, {"reminder": handler})
table.enqueue(
"reminder",
{"email": "founder@example.com"},
start + timedelta(days=1),
)
runner.tick()
assert sent == []
clock.advance(24 * 60 * 60)
runner.tick()
assert len(sent) == 1
assert sent[0]["to"] == "founder@example.com"
assert sent[0]["sent_at"] == clock.now().isoformat()
Run that file with a local pytest command. The assertion is the product contract in code. Agents can change handlers only if this stays green.
python -m pytest test_reminders.py -q
One leaked datetime.now call still hurts. A founder can learn that after launch day. The subject line used the wall clock instead.
Tests stayed green because the leak was tiny. Users in other timezones saw the wrong copy. The fix was another injection, not a new queue.
A second guard scans the tree for queue words. It is a blunt grep written in Python. Blunt tools still fit a kitchen ship.
# forbid_queues.py
from pathlib import Path
BANNED = ("celery", "redis", "rq", "bullmq", "sidekiq", "sqs", "cloudtasks")
def scan(root: Path) -> list[str]:
hits = []
for path in root.rglob("*.py"):
if path.name == "forbid_queues.py":
continue
text = path.read_text(encoding="utf-8").lower()
for name in BANNED:
if name in text:
hits.append(f"{path}: {name}")
return hits
if __name__ == "__main__":
import sys
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
hits = scan(root)
for hit in hits:
print(hit)
raise SystemExit(1 if hits else 0)
False positives will appear inside comments and docs. The founder reads each hit before merging code. A banned word is a conversation, not a trophy.
python forbid_queues.py .
The coding model needs a small world file. The file states the clock and the table. It also names the services that remain forbidden.
{
"clock": "KitchenClock",
"jobs": "sqlite",
"mail": "injected port",
"forbidden": ["redis", "celery", "hosted cron"]
}
The founder pastes that file into the coding session. The model fills handlers against those ports only. The tests in the repo stay in charge afterward.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project with free model access and a free server option. Indie founders can try those options while the job table stays local.
The model writes reminder handlers against KitchenClock. A free server can run pytest on each push. The SQLite file and the clock stay in the repo.
This is not a claim about model names or quotas. It is a workflow with a local contract. The contract is the tests plus world.json.
A single-process loop can tick in production. It sleeps on the real wall clock. Tests never call this loop at all.
# tick_loop.py
import time
def run_forever(runner, sleep_seconds=30):
while True:
runner.tick()
time.sleep(sleep_seconds)
Thirty seconds is late for stock trades. It is fine for a trial reminder email. Indie software should pick that honesty early.
Start the loop only after the tests pass. Keep the database path on local disk. Do not hide a second clock inside handlers.
python -c "from tick_loop import run_forever; raise SystemExit('wire JobRunner first')"
Limitations arrive as soon as traffic grows. SQLite locking will hurt concurrent writers fast. A second app instance will double-send mail.
The due query has no skip-locked clause today. Two ticks can grab the same row. The mark_done update does not detect races.
Failed handlers currently stay due forever after errors. That pattern can hammer a broken mailer. A later column for attempts would slow retries.
The import scan does not parse Python syntax. It matches substrings inside comments and strings. It will miss a crafted dynamic import.
Timezones remain a human problem, not a library trick. Store UTC in run_at and done_at. Render local time only at the mailer port.
Who should skip this approach is fairly clear. Teams with legal timing on notices need brokers. Products that fan out across regions need real queues.
Multi-tenant workers also outgrow a kitchen clock. Jobs that must survive deploys mid-tick do too. Those teams should rent a proper queue later.
Solo founders shipping a first paid loop benefit here. They keep the Saturday bill at zero for now. They accept the limits in the public README.
The Saturday scene ends with a quieter diff. The reminder feature is a table and a test. The freight truck stays parked at the depot.
Top comments (0)