EngineeringJune 2, 2026·3 min read

A 30-second poller beats a message queue here.

Booking a future AI call is a scheduling problem, and the textbook answer is a queue. For a single bedside fleet, a SQLite table and a background loop is the right amount of machinery.

When a family member books a call for 4pm tomorrow, something has to remember to place it. The reflexive engineering answer is a job queue — Celery, RQ, a Redis broker, a worker fleet, a dead-letter path. That stack exists for a reason, and none of those reasons apply to us yet.

What we actually have is a backlog of future calls that needs to be checked on a clock and fired when its time arrives. That is a row in a table and a loop. So that is what we built.

The whole scheduler.

A scheduled call is a SQLite row with a phone number, an agent, a fire-at timestamp, and a status. A single background task wakes every 30 seconds, selects the rows that are due and still pending, places each call, and flips the status. WAL mode keeps the poller's reads from blocking the request path's writes. There is no broker and no second process to babysit.

while True:
    now = int(time.time())
    due = db.execute(
        "SELECT * FROM scheduled_calls "
        "WHERE fire_at <= ? AND status = 'pending'",
        (now,),
    ).fetchall()
    for call in due:
        place_call(call)
        mark(call.id, "placed")
    await asyncio.sleep(30)

Thirty seconds of granularity is invisible to a human who booked a call yesterday. Nobody notices, or cares, whether their 4pm call lands at 4:00:00 or 4:00:24. The looseness that would be a defect in a trading system is free precision we get to not pay for.

Knowing what we traded away.

This is not free, and pretending it is would be the real mistake. The database lives on ephemeral disk unless a volume is mounted, so a pod restart between booking and fire-time drops the call. There is exactly one poller, so it is a single point of failure and it does not scale horizontally. If two replicas ran this loop unguarded, they would both place the same call.

Every one of those is a real limitation and none of them is a problem at one wing, one pod, a handful of scheduled calls a day. The job of an early backend is to match its machinery to its load, not to the load it might have at a thousand facilities. A queue would be more correct in the abstract and more wrong in practice — more moving parts to break, monitor, and explain, defending against failure modes we do not have.

The upgrade path is written down.

The trigger to revisit this is specific: a persistent volume the first time a dropped call actually costs us, and a Postgres row lock or a real queue the day we run a second backend replica. Until one of those is true, the simplest thing that survives a code review is the correct thing. We would rather earn the complexity than pre-pay for it.

backendschedulingsqlitesimplicity

See it in a wing

30 days. One wing. Your numbers.

Ten Companion units, cellular preconfigured, ready in week one. Weekly outcome reports auto-emailed.

Schedule a 20-minute call →