DEV Community

kongkong
kongkong

Posted on

Admit the Event Before the Free Model Touches It

I keep replaying one ugly afternoon whenever a webhook and a free model share the same request. The billing provider retried, the free server restarted, and two explanations landed for one event that nobody had firmly admitted. A zero price had become the excuse to call the model before the database said yes. Was I debugging a flaky model, or was the request path doing a job it should never own?

That replay is why my opinion stays sharper than a shrug about free tiers and cheap hosts. I want the event admitted in a durable outbox before any free model is allowed to see the payload. The free server should run that same admission route and one worker, not a side script with its own key. If the invoice stays silent, do you honestly believe the duplicate write will stay silent too?

Disclosure: This article was prepared as part of MonkeyCode's product outreach. I mention MonkeyCode here only because its free model access and its free server option fit this admission slice. A vendor name does not make an inline model call safe inside a short webhook timeout. I am not naming a model, a token quota, a machine size, or a period for which free access continues.

If those facts matter for your rollout, check the current product notes before you freeze a design on them. My claim is about ownership, and it would stand even if you swapped the free options for paid ones tomorrow. Admit the foreign event first, return as soon as the outbox row exists, and only then spend a model call. Why would you spend a fragile free call before you can prove the event will not arrive again?

The layer that actually breaks

Picture a billing webhook that carries a provider event id, an account id, and a short reason code. The handler in that prototype calls the model, waits for a sentence, and tries to write it before returning 200. When the proxy cut the socket, the provider retried, and the second call wrote a second sentence beside the first. The first broken layer was admission, not the wording, because the database had never claimed the event id.

I want a boring outbox table more than I want a clever prompt sitting on this boundary. The row stores the provider event id as a unique key, a payload hash, a status, and the account it belongs to. Status moves from admitted to running to labeled, or it stops at empty or failed without pretending success. A unique key is a better guard than a memory lock, because a free server restart throws memory away.

create table billing_events (
  provider_event_id text primary key,
  account_id text not null,
  payload_hash text not null,
  status text not null,
  label_text text not null default '',
  claimed_at timestamptz
);
Enter fullscreen mode Exit fullscreen mode

The admission route should feel dull, and dull is a real compliment when webhooks get involved. It checks a shared secret, inserts the outbox row, and returns 202 with the event id and nothing else. If the insert hits the unique constraint, it returns 202 again and does not call the model on the replay. Can you explain your webhook to a reviewer if success sometimes means the model ran and sometimes does not?

One seam, then the call

The sketch below is a proposal I would put behind that route, and it is not a trace from a live host. I keep the provider client behind one function so the free model cannot leak into the webhook module. The worker claims a row by moving it to running, and a second worker must see zero rows to claim. If you cannot describe that claim in one sentence, you are not ready to point a free server at it.

# Proposal sketch, not a captured trace from a live host.
import hashlib
from enum import Enum

class EventStatus(str, Enum):
    ADMITTED = 'admitted'
    RUNNING = 'running'
    LABELED = 'labeled'
    EMPTY = 'empty'
    FAILED = 'failed'

def payload_hash(raw: bytes) -> str:
    return hashlib.sha256(raw).hexdigest()

def admit_event(store, account_id: str, provider_event_id: str, raw: bytes):
    inserted = store.insert_once(
        account_id=account_id,
        provider_event_id=provider_event_id,
        payload_hash=payload_hash(raw),
        status=EventStatus.ADMITTED,
    )
    if not inserted:
        return store.status_of(provider_event_id)
    return EventStatus.ADMITTED

def claim(conn, provider_event_id: str, expected: str, nxt: str) -> bool:
    cursor = conn.execute(
        '''
        update billing_events
           set status = ?, claimed_at = current_timestamp
         where provider_event_id = ?
           and status = ?
        ''',
        (nxt, provider_event_id, expected),
    )
    return cursor.rowcount == 1

def label_once(store, client, provider_event_id: str):
    claimed = store.claim(
        provider_event_id,
        EventStatus.ADMITTED,
        EventStatus.RUNNING,
    )
    if not claimed:
        return store.status_of(provider_event_id)
    try:
        text = client.explain(
            store.payload_for(provider_event_id),
            timeout_ms=8000,
        )
    except TimeoutError:
        store.mark(provider_event_id, EventStatus.FAILED, text='')
        return EventStatus.FAILED
    if not text or not text.strip():
        store.mark(provider_event_id, EventStatus.EMPTY, text='')
        return EventStatus.EMPTY
    store.mark(provider_event_id, EventStatus.LABELED, text=text.strip())
    return EventStatus.LABELED
Enter fullscreen mode Exit fullscreen mode

I map empty model text to an empty status because silence is not a label a support page should display. I map a timeout to failed only after the row records the attempt, so a restart can decide whether to retry. I do not retry inside the webhook, since the provider will retry admission and the outbox already dedupes that. A retry belongs to the worker, and only when the row is still admitted or explicitly marked for another pass.

@app.post('/webhooks/billing', status_code=202)
async def admit_billing(request: Request, store=Depends(get_store)):
    raw = await request.body()
    signature = request.headers.get('x-signature', '')
    if not signature_ok(raw, signature, settings.webhook_secret):
        raise HTTPException(status_code=401, detail='bad signature')
    event = parse_billing_event(raw)
    status = admit_event(store, event.account_id, event.provider_event_id, raw)
    return {'provider_event_id': event.provider_event_id, 'status': status}
Enter fullscreen mode Exit fullscreen mode

When I use the free server option, I want one image that serves the admission route and the worker loop. A second script with a copied key is how prototypes grow a private back door that nobody monitors. The same migration must create the outbox before the process listens, or the host is not actually running the slice. Would you call it deployed if a restart loses every event that had not yet reached the model?

The page never asks the model anything, and that separation is the part I would defend in review. It reads the outbox by account and event id, shows admitted as pending, and shows labeled only when text exists. Auth on that read must match the account on the row, or a free server becomes a window onto someone else's events. If the UI polls the webhook response body instead, you have quietly moved the model back onto the hot path.

@app.get('/accounts/{account_id}/events/{provider_event_id}')
def read_event(account_id: str, provider_event_id: str, actor=Depends(current_actor), store=Depends(get_store)):
    if actor.account_id != account_id:
        raise HTTPException(status_code=403, detail='wrong account')
    row = store.get(account_id, provider_event_id)
    if row is None:
        raise HTTPException(status_code=404, detail='missing event')
    label = row.text if row.status == EventStatus.LABELED else ''
    return {'status': row.status, 'label': label}
Enter fullscreen mode Exit fullscreen mode

Prove the replay before the host does

Before that host sees traffic, I run the same checks on my laptop so the free tier is not my test suite. The pytest file should cover a fresh admit, a duplicate event id, an empty completion, and a worker crash after the claim. Curl should show 202 for the first post and 202 for the immediate replay, with one row in the table. If those commands need a different env file on the server, I treat that drift as a bug rather than a convenience.

python -m pytest tests/test_webhook_outbox.py -q
alembic upgrade head
uvicorn app.main:app --host 0.0.0.0 --port 8000
python -m app.worker
curl -sS -D - -o /tmp/admit.json \
  -H 'content-type: application/json' \
  -H "x-signature: $WEBHOOK_SIG" \
  --data '{"id":"evt_1","account":"ac_9","reason":"past_due"}' \
  http://127.0.0.1:8000/webhooks/billing
Enter fullscreen mode Exit fullscreen mode

The duplicate test matters more than a happy-path demo, because free inference makes retries feel harmless. I assert that the client mock was called once, even though the webhook handler was invoked twice with the same id. The crash test kills the worker after the status becomes running, then starts it again and expects no second label. A green process is not evidence, and I want the row count to stay at one when the world gets rude.

def test_replay_does_not_call_model_twice(store, client):
    raw = b'{"id":"evt_1","account":"ac_9","reason":"past_due"}'
    first = admit_event(store, 'ac_9', 'evt_1', raw)
    second = admit_event(store, 'ac_9', 'evt_1', raw)
    assert first == EventStatus.ADMITTED
    assert second == EventStatus.ADMITTED
    label_once(store, client, 'evt_1')
    label_once(store, client, 'evt_1')
    assert client.calls == 1
    assert store.count(provider_event_id='evt_1') == 1
Enter fullscreen mode Exit fullscreen mode

I chose an outbox over an in-request call because webhook timeouts and model latency do not share a clock. I chose a provider event id over a random job id because the sender already gave me the idempotency key. I chose one worker loop over a fan-out queue because this slice does not need a broker to prove the contract. The approach I would reject is streaming the label back on the webhook response, since the sender is not a person waiting.

Who should leave this alone

This is the wrong shape if a person is staring at a spinner and needs the label before the next click. It is also wrong if you must fan the same event across several models and merge their answers in one transaction. Do not park raw payment details, secrets, or regulated text in an outbox just because the server happens to be free. Free model access and a free server can change, throttle, or disappear, and I will not invent a calendar for that.

If you came for a leaderboard, a hardware story, or proof that a free model beats a paid one, this will disappoint you. I am defending a handoff, not a quality claim, and I have not run a benchmark you should quote. Skip the pattern when your compliance review forbids storing the payload hash next to an account id on a shared host. A small internal tool can still use it, as long as the same tests pass on the laptop and on the free server.

The slice is honest when admission is durable, the model sits behind one seam, and replays do not mint new labels. Until the duplicate test fails closed, a live call on a free host is just a demo with a public address. If you press this into your own webhook, tell me which handoff feels least stable under a real replay. Is it the unique insert, the worker claim, the provider timeout, or the free server restart, and what status code came back?

Top comments (0)