# Enforcing an allowance, end to end

The last unit of an allowance is claimed by a conditional update, so two simultaneous attempts cannot both win it. The one that loses gets a coded refusal before any provider is called. Nothing it was carrying is discarded: the draft stays, the approval stays recorded, and mail ingestion holds its read position. At the next daily reset the same work proceeds untouched.

- **Status:** Available
- **Audience:** customer
- **In the app:** #/billing, #/needs-you
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/allowance-enforcement/

## The approach

Enforcement is not a check followed by an action — that pattern loses the race between two requests reading the same remaining count. The reservation *is* the check: today's usage row is claimed if it does not exist, then incremented by a conditional update that only succeeds while there is room. Whichever request the database serves second is refused, and it is refused before anything has been handed to a provider.

A caller that reserves repeatedly against one workspace — the mail sync, reserving once per message — reads the plan facts once for the batch, because a transaction sees the same plan for every message in it. What is never batched is the reservation itself: the conditional update runs for each unit, and that is the part that makes the limit authoritative.

## The refusal

| Reason | What happened | Clears when |
|---|---|---|
| Daily allowance spent | Today's count reached the plan's limit for that dimension | The next daily reset, on the UTC date boundary |
| Plan not active | The entitlement is not in an active or trialing state | The plan is settled — see [A plan lapsing](/docs/workflows/plan-lapse/) |
| Dated plan ended | A plan with an end date has passed it | The plan is renewed or changed |
| Resource ceiling reached | One more of a thing cannot exist — a mailbox slot, for instance | Something is removed, or the plan allows more |

Each refusal carries its own code rather than a single generic message, which is what lets a screen say *the allowance is spent* instead of *try again*. The distinction matters because the two conditions have different waiting times: one clears on a clock, the other does not clear at all without somebody acting.

## Stage by stage

1. Trigger — a metered action reaches the reservation with the day's allowance already at its limit.
2. User or external event — often nobody is watching. The engine's own drain hits the limit far more often than a person pressing send does.
3. Authentication and workspace resolution — already settled; the reservation refuses outright if the plan facts it was handed belong to a different workspace than the one being charged.
4. Ingest or request — the caller asks for one unit of a named dimension inside its own transaction.
5. Canonical record — the conditional update does not match, so today's usage row is unchanged. A refusal writes nothing except the record of itself.
6. Reasoning — none is spent, on purpose. Drafting a reply that cannot be sent costs a model call and produces something nobody may use.
7. Knowledge, memory and rules — untouched.
8. Autonomy and approval — unchanged. A held item does not lose its approval because the send was refused; the approval was a decision and it stands.
9. Action through a provider — none. The refusal happens before provider work, which is why there is never a half-sent message to reconcile.
10. Result — a coded refusal, and the item stays exactly where it was in the queue.
11. Relationship, timeline and memory — the conversation is not marked answered. A refused send never becomes a sent one in anybody's history.
12. Audit, usage and Needs You — the refusal is recorded as a decision with its reason, and the blockage surfaces for a person rather than only in a log.

## What is held

**An approved draft** — Stays approved and stays queued. Approving again does nothing useful; the item is waiting on allowance, not on a decision.
**The mail read position** — Does not advance over refused messages. Advancing it would mark a window done and the refused mail would never be fetched again — not late, gone.
**A phone follow-up** — Left for a later tick rather than closed, unless it is more than a day late, in which case it is closed as missed and never rung. A very late call-back is worse than none.
**An inbound message already delivered** — Kept. Inbound is counted for reporting and is never used to refuse.

> **Careful** Holding is not the same as freezing. A held reply answers the thread as it stood when it was written, and threads move. When work resumes after a long hold, read before releasing — regenerating is cheap, and sending a stale answer is not.

## The resume

1. Wait for the daily reset, or change the plan so the limit is higher.
   - Result: The next reservation succeeds. There is no restart, no cache to clear and no queue to rebuild.
2. Let the mail sync run again.
   - Result: It resumes from the position it held, so the refused messages are ingested in order.
3. Check the held items before releasing a batch of them.
   - Result: Anything the customer has already followed up on is caught before it goes out as a duplicate answer.

A reservation that succeeded for work that then failed is released rather than left spent — otherwise a provider outage would consume a whole day's allowance on messages nobody ever received, which is a limit doing damage rather than its job.

## Questions

### When exactly does the allowance reset?

On the UTC date boundary. The usage row is keyed by workspace, dimension and UTC date, so a new date is simply a new row starting at zero — there is no scheduled job to fail and nothing to sweep.

### If two sends race for the last unit, can both go out?

No. The claim is a conditional update rather than a read followed by a write, so exactly one of them succeeds and the other is refused before it reaches a provider. That property is the reason the check and the increment are the same statement.

### Does hitting an allowance affect what Connect may do?

No. Autonomy is unchanged: a workspace on an autonomous channel is still autonomous, it simply has nothing left to spend. Permission and allowance are separate systems and neither one silently edits the other.

## Related

- [Metering usage, end to end](https://connectbyjbrh.com/docs/workflows/usage-metering/)
- [Usage and allowances](https://connectbyjbrh.com/docs/account/usage/)
- [The allowance is spent](https://connectbyjbrh.com/docs/troubleshooting/allowance-reached/)
- [The daily email allowance](https://connectbyjbrh.com/docs/email/daily-allowance/)
- [Hitting the daily allowance, end to end](https://connectbyjbrh.com/docs/workflows/quota-refusal/)
- [How a mail cursor loses messages, and how to stop it](https://connectbyjbrh.com/research/cursor-loss/)

## What this page is based on

- `backend/app/tenant_entitlements.py` — the conditional reservation and release_daily_usage
- `backend/app/tenant_inbox.py` — the quota stop that does not move the cursor
- docs-source/sources/GENERAL.md §7 — the phone follow-up drain and its late rule
- `backend/app/metering.py` — dimensions and their enforcement sites
