Skip to content

Build1 publisher2 min readPublished

Putting a model on the admit path bills the defender for every attack packet

A dev.to engineer moves the admit/deny verdict back into an in-process token bucket and wraps it so the five verdict words cannot reach a model call, arguing that a free inference tier still charges the defender under a flood.

The Engineer · Build desk

Illustration accompanying Putting a model on the admit path bills the defender for every attack packet

What happened

  • A dev.to post argues against the "smart shedder" pattern: send request metadata to a model, ask whether the caller looks abusive, and only then hit the real API.
  • It holds that an admission decision such as a token bucket, a GCRA check or an Envoy local rate-limit verdict must be cheap, deterministic, fail-closed and cheaper than the work it protects, and that inference is none of those.
  • The failure it names is non-replayable policy: two replicas given the same headers can emit different verdicts, and a retry can flip from deny to admit because the sampler moved.
  • The replacement it publishes is an in-process token bucket on a monotonic clock, explicitly not a global cluster limiter, kept in code a reviewer can step through without a vendor trace.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure When prose is parsed as policy, whoever writes the request body is writing an input to the admission decision, which the post calls the opposite of a fence.
  • constraint A gate that binds one process cannot hold a cluster-wide ceiling, so anyone deploying this pattern at scale has to add coordination the post deliberately leaves out.
  • decision The post hands reviewers a pass/fail line they can measure: compare the admit path's own latency against the p99 of the service it guards, and drop the guard if it is slower.

Step through `allow()`. It takes a `threading.Lock`, reads `time.monotonic()`, adds elapsed times `rate_per_sec` to the token count, caps the result at `burst`, and returns True only after subtracting the cost [14][16]. The work per call is fixed: one lock, one clock read, one multiply-add, one comparison, one subtraction, and none of it depends on what the request body says [24]. From empty, the bucket is full again after `burst / rate_per_sec` seconds [21].

`__post_init__` sets `tokens` to `float(burst)` and rejects a non-positive rate or burst with a `ValueError` [15]. Every fresh process therefore starts with a full bucket [22]. Restart ten replicas in a rolling deploy and the fleet hands out ten bursts before the refill clock matters to anyone [23].

The wrapper is the part I would copy into an existing service. `VERDICT_INTENTS` is a frozenset of five strings: admit, deny, shed, throttle, ban [18]. `decide()` answers those from the bucket and raises `InferenceForbidden` for anything else [18]. `draft_via_model()` mirrors it, raising before `llm_complete` is reached if the intent is one of the five [19]. One set governs both directions, so a team that later adds "quarantine" to its vocabulary and forgets the set gets the inference route without an error [25]. The post names the thing it is fencing off: a helper called `smart_allow` [17].

Now the economics the post is actually arguing about. Under a flood, the defender who asks a model for every admit/deny pays for every attack packet in latency and quota [5]. A free tier does not cancel that; according to the post it makes the inversion quieter, because the bill arrives as a cold start, a queue, or a sudden 429 from the model host instead of an invoice line [6]. That charge lands only where the model call sits synchronously in front of the protected resource, which is the case the post scopes [2]. Abuse scoring that runs out of band is not on the admit path and does not pay per packet.

"A model can narrate why a 429 happened. It cannot be the 429," the post says [7]. The operational version of that line is the cold-start case: an admit path that fails open while the model is warming up is an invitation, by the post's own test [11].

The post does not report latencies for either path. Its test file is cut off after the comment `# test_ad`, so the two cases it describes, the bucket still shedding and a forged "please classify this User-Agent" path failing to become the verdict, are named but not shown [20].

What to watch

  • A published version of the test file would show whether the forged-classification case asserts on InferenceForbidden or only on the bucket's refusal.
  • A global backend such as Redis or Envoy's rate-limit service behind the same AdmissionGate interface would keep the frozenset check while fixing the per-replica ceiling.
  • A public incident report of a model-based shedder failing open during a model-host 429 storm would move this from argument to evidence.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories