cd /news/ai-agents/the-lease-loop-is-not-a-chat-complet… · home topics ai-agents article
[ARTICLE · art-134464] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

The Lease Loop Is Not a Chat Completion

A developer argues that distributed lease and leader-election loops should rely on deterministic fencing tokens rather than LLM chat completions, citing latency, non-determinism, and vendor-controlled safety layers as failure modes. The post provides a worked PostgreSQL example with a worker_leases table and a Python renewer that increments an epoch on a deadline and refuses writes when the lease is lost.

by read9 min views1 publishedSep 19, 2026

A fencing token is not a paragraph. It is a counter that must rise on a clock, die on a deadline, and never ask a model whether the holder still deserves the lock. Free inference has no place in that loop.

Teams keep stuffing chat completions into leader election anyway. A node goes quiet. Logs look messy. Someone wires a cheap model to read the noise and vote on who should keep the lease. The analogy is a lighthouse that phones a critic before each flash. The critic is witty. The reef does not care.

Latency is the first crack. Lease renewal is a budget measured in hundreds of milliseconds, not in token streams. A free path can stall, queue, or return a truncated object when the provider is busy. The lock expires while the prose is still arriving. Split brain is not a literary device. It is two writers sharing one offset.

Non-determinism is the second crack. The same health snapshot can yield keep, drop, or a sentence that fails JSON. Control planes need the same inputs to produce the same fence. A model that hedges is not a quorum. It is a coin with extra faces.

The third crack is authority. A completion can be filtered, rate limited, or rewritten by a safety layer the caller does not own. Leader election that depends on a vendor's refusal class has already given that vendor a veto over the write path. The veto stays invisible in dashboards that only chart HTTP 200.

A lease is a timed exclusive. The holder proves liveness by raising an epoch before expiry. Downstream storage must reject any write whose epoch is stale. The model of the world is small: holder id, epoch, expiry. Nothing in that tuple is improved by a temperature setting.

PostgreSQL can carry a fencing token without ceremony. The schema and renewer below are a worked example, not a multi-region consensus protocol. Treat them as labeled sample code.

CREATE TABLE worker_leases (
  name        text PRIMARY KEY,
  holder      text NOT NULL,
  epoch       bigint NOT NULL,
  expires_at  timestamptz NOT NULL
);

CREATE INDEX ON worker_leases (expires_at);
python
import os
import socket
import time
import psycopg

LEASE_NAME = "order-writer"
TTL_SECONDS = 15
RENEW_EVERY = 5
HOLDER = f"{socket.gethostname()}:{os.getpid()}"
DSN = os.environ["LEASE_DATABASE_URL"]

ACQUIRE = """
INSERT INTO worker_leases (name, holder, epoch, expires_at)
VALUES (%s, %s, 1, now() + make_interval(secs => %s))
ON CONFLICT (name) DO UPDATE
SET holder = EXCLUDED.holder,
    epoch = worker_leases.epoch + 1,
    expires_at = EXCLUDED.expires_at
WHERE worker_leases.expires_at < now()
RETURNING epoch;
"""

RENEW = """
UPDATE worker_leases
SET epoch = epoch + 1,
    expires_at = now() + make_interval(secs => %s)
WHERE name = %s
  AND holder = %s
  AND expires_at > now()
RETURNING epoch;
"""

def acquire(conn):
    with conn.cursor() as cur:
        cur.execute(ACQUIRE, (LEASE_NAME, HOLDER, TTL_SECONDS))
        row = cur.fetchone()
        conn.commit()
        return None if row is None else int(row[0])

def renew(conn):
    with conn.cursor() as cur:
        cur.execute(RENEW, (TTL_SECONDS, LEASE_NAME, HOLDER))
        row = cur.fetchone()
        conn.commit()
        return None if row is None else int(row[0])

def hold():
    with psycopg.connect(DSN) as conn:
        epoch = acquire(conn)
        if epoch is None:
            raise SystemExit("lease held elsewhere")
        while True:
            time.sleep(RENEW_EVERY)
            epoch = renew(conn)
            if epoch is None:
                raise SystemExit("lost lease; refuse all writes")
            print(f"fence={epoch}", flush=True)

if __name__ == "__main__":
    hold()

The control flow is the design. Acquire or renew returns an integer fence, or it returns nothing. There is no branch that asks a model to interpret the row. Writers that use the lease must send that epoch with every mutating call. A storage layer that cannot see the fence is not protected by the loop.

def append_order(conn, epoch, payload):
    with conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO orders (payload, fence)
            SELECT %s, %s
            WHERE EXISTS (
              SELECT 1 FROM worker_leases
              WHERE name = %s
                AND holder = %s
                AND epoch = %s
                AND expires_at > now()
            )
            """,
            (payload, epoch, LEASE_NAME, HOLDER, epoch),
        )
        if cur.rowcount != 1:
            conn.rollback()
            raise RuntimeError("stale fence; drop the write")
        conn.commit()

A free completion that explains why the insert failed is optional theatre after the fact. It must not retry the insert, bump the epoch, or pick a new holder. Commentary can wait. Membership cannot.

Opinion is cheap. Import graphs are not. The next artifact fails CI when the lease package grows an inference client. It is a proposed gate, not a claim that a production fleet already runs it.

from pathlib import Path
import ast

LEASE_DIR = Path("lease")
FORBIDDEN_IMPORTS = {
    "openai",
    "anthropic",
    "httpx",
    "aiohttp",
    "requests",
    "google",
}
FORBIDDEN_NAME_FRAGMENTS = (
    "chat.completions",
    "messages.create",
    "generate_content",
)

def _imported_roots(tree: ast.AST) -> set[str]:
    roots = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                roots.add(alias.name.split(".")[0])
        elif isinstance(node, ast.ImportFrom) and node.module:
            roots.add(node.module.split(".")[0])
    return roots

def test_lease_modules_stay_offline():
    files = list(LEASE_DIR.glob("*.py"))
    assert files, "lease package missing"
    offenders = []
    for path in files:
        text = path.read_text(encoding="utf-8")
        tree = ast.parse(text)
        bad = _imported_roots(tree) & FORBIDDEN_IMPORTS
        hits = [frag for frag in FORBIDDEN_NAME_FRAGMENTS if frag in text]
        if bad or hits:
            offenders.append(f"{path}: imports={sorted(bad)} calls={hits}")
    assert not offenders, "inference reached the lease path:\n" + "\n".join(offenders)

Run it the boring way.

mkdir -p lease
cp lease_loop.py fenced_write.py lease/
python -m pytest test_lease_has_no_inference.py -q

The test is heuristic. A determined engineer can hide a completion behind a local wrapper named health.py. That is why the gate is an exit criterion, not a personality test. If the wrapper appears, the lease package is already on the wrong side of the line. Metrics exporters belong in another process, so the import ban does not become an excuse to smuggle a model through a telemetry helper.

A free model is a useful intern for reading a postmortem. It is a reckless intern for holding the flashlight. The red flag is not "an LLM exists in the company." The red flag is a prompt that can change membership. If the text of a completion can extend a lease, drop a peer, or choose which replica drains a queue, the design has confused explanation with authority.

Timeout arithmetic is the next flag. If the model's p95 exceeds half the TTL, the loop is already insolvent. Padding the TTL to wait for tokens just lengthens the split-brain window. Schema drift in the reply is a third. Control messages that need a JSON repair step do not belong next to a fencing token. A fourth flag is softer and easier to miss: a dashboard that treats "the model said the node looks sick" as equivalent to a missed heartbeat.

Better alternatives are dull on purpose. A leases table plus make_interval covers a single primary. PostgreSQL advisory locks cover even smaller footprints. etcd elections and Consul sessions cover a cluster that already speaks consensus. ZooKeeper remains a valid, if heavy, fencing service. None of these tools ask for a system prompt, and none of them improve when the invoice for tokens is zero.

Use the table as a stop/go card during review, not as a personality quiz.

Observation Stay on the local lease Exit the design
Renewer import graph includes an LLM or generic HTTP client Keep shipping the SQL loop Fail closed; strip the client
p95 of any completion exceeds half the TTL Do not add a model to buy time Remove the model; shorten the path
Failure drill succeeds with inference unreachable Model is commentary only Keep commentary off the renewer
Failure drill fails without inference The model was holding the fence
A human cannot state the fence rule in one sentence without mentioning a model Rewrite the sentence until the model disappears Stop the rollout

Exit when the renewer imports a network client that is not the database. Exit when a human cannot state the fence rule without mentioning a model. Exit when a failure drill cannot be run with the inference provider unreachable. If the drill cannot pass in that condition, inference was never optional.

Scratch generation is a different job. A reviewer may want extra negative fixtures: a truncated JSON body, a 429, a refusal, a slow stream. Those fixtures can be drafted on a disposable box.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option are a reasonable place to generate those fixtures and to run the import checker against a throwaway clone. They are not a reasonable place to host lease_loop.py. The lease either talks to Postgres, or it does not talk.

The example ignores clock jump, long garbage-collection s, and network partitions that leave the SQL session half-open. now() inside PostgreSQL is better than the client's wall clock, and it is still not Raft. Teams with a multi-region write path need a real consensus group, not a Python while True.

The CI checker does not prove liveness. It only forbids a class of imports. It will miss a sidecar that renews the lease over a Unix socket to an agent. Treat that sidecar as part of the lease package and apply the same rule. The checker will also nag a legitimate requests call if someone parks it in lease/; move the call, do not weaken the gate.

Free inference is the wrong owner of adjacent decisions that look like commentary and act like membership. Draining a Kafka partition, choosing which replica is mostly caught up, or rewriting a readiness probe because logs seem unhealthy are fencing problems in costume. The same stop/go card applies.

Operators who already run etcd or ZooKeeper and never call a model from the election path can skim the checker and move on. Researchers studying lock contention on synthetic traces may use a model offline, against recorded data, with no ability to write epoch. That study is not a control loop.

The approach is a poor fit for product demos that exist to show an agent running the cluster. A demo can narrate a lease. It should not hold one. It is also a poor fit for anyone hoping a free completion will replace a quorum because the invoice is zero. The invoice is not the failure domain. Dual writers are.

A lease loop that stays boring will look under-engineered in a screenshot. That is the point. The lighthouse does not workshop the next flash. It flashes, or it goes dark, and the chart records the gap.

── more in #ai-agents 4 stories · sorted by recency
── more on @postgresql 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-lease-loop-is-no…] indexed:0 read:9min 2026-09-19 ·