---
title: "When the limit is hit"
description: "What a caller sees when a shared impact limit refuses a governed action: the status, the body, the effect, and what an operator can do next."
---

A [shared limit](/execution/impact-limits) is reserved immediately before dispatch. When it has no room, the call is refused before anything is sent. This page shows what comes back.

The responses below come from Anlyon's CI test runs.

## What the caller sees

**The HTTP status is 200.** The refusal is in the body, not in the status code. A caller that checks the status code alone will miss it. Read `status` and `grade`.

The scenario: a limit of 5 `emails` per day. One key sends to 3 recipients. A second key then asks for 3 more, with 2 left.

```typescript TypeScript
const { data } = await agent.actions.invoke('send-email', {
  emailId: 'welcome-2',
  to: ['a@example.com', 'b@example.com', 'c@example.com'],
  subject: 'Welcome',
});

if (data!.grade === 'refused') {
  console.log(data!.status, data!.error);
}
```

```python Python
result = agent.actions.invoke(
    "send-email",
    {"emailId": "welcome-2", "to": ["a@example.com", "b@example.com", "c@example.com"], "subject": "Welcome"},
)

if result.data["grade"] == "refused":
    print(result.data["status"], result.data["error"])
```

```bash curl
curl -s -X POST https://api.anlyon.com/api/v2/actions/send-email/invoke \
  -H "Authorization: Bearer $ANLYON_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-2" \
  -d '{ "input": { "emailId": "welcome-2", "to": ["a@example.com", "b@example.com", "c@example.com"], "subject": "Welcome" } }'
```

```json
{
  "status": "failed",
  "grade": "refused",
  "error": "Dispatch refused (impact_limit_exceeded): Not enough allowance: daily emails needs 3, has 2."
}
```

The error names `impact_limit_exceeded` and lists every limit that fell short, with what the call needed and what was available. A call under several limits reports each shortfall in the same sentence.

The same sequence was repeated with a second API key and then with a caller that has no key. Neither brought a new allowance.

## The effect

The refused call still leaves an effect. Read it with the `effectId` from the response.

```typescript TypeScript
const { data: effect } = await agent.actions.effect(data!.effectId!);
console.log(effect!.stage, effect!.outcome, effect!.rejectionReason, effect!.attempts.length);

const { data: refused } = await agent.actions.effects({ grade: 'refused', action: 'send-email' });
```

```python Python
effect = agent.actions.effect(result.data["effectId"]).data
print(effect["stage"], effect["outcome"], effect["rejectionReason"], len(effect["attempts"]))

refused = agent.actions.effects(grade="refused", action="send-email").data
```

```bash curl
curl -s https://api.anlyon.com/api/v2/actions/effects/eff_01JABCDEF \
  -H "Authorization: Bearer $ANLYON_AGENT_KEY"

curl -s "https://api.anlyon.com/api/v2/actions/effects?grade=refused&action=send-email" \
  -H "Authorization: Bearer $ANLYON_AGENT_KEY"
```

```json
{
  "stage": "rejected",
  "outcome": "not_dispatched",
  "rejectionReason": "impact_limit_exceeded",
  "grade": "refused",
  "verification": "not_dispatched",
  "attempts": []
}
```

Zero attempts means nothing was sent to the provider. The effect's evidence has an entry that begins "Not dispatched". It carries the shortfalls in its details.

From the CLI: `anlyon effects list --action send-email --grade refused` and `anlyon effects get <id>`. An invoke that is refused exits 14.

## The allowance is untouched

Reservation is all or nothing. If one applicable limit has no room, none is reserved. The refused call above asked for 3 with 2 left. It did not take the 2.

```typescript TypeScript
const { data: limit } = await agent.impactLimits.get('lim_01JABCDEF');
console.log(limit!.consumed, limit!.reserved, limit!.available);
```

```python Python
limit = agent.impact_limits.get("lim_01JABCDEF").data
print(limit["consumed"], limit["reserved"], limit["available"])
```

```bash curl
curl -s https://api.anlyon.com/api/v2/impact-limits/lim_01JABCDEF \
  -H "Authorization: Bearer $ANLYON_AGENT_KEY"
```

A third call, to 2 recipients, then succeeded. The limit read:

```json
{ "consumed": 5, "reserved": 0, "available": 0 }
```

Three calls were made and the destination saw two writes.

Reading limits needs `impact-limits:read`, which is in the default set. An agent can check `available` before it asks.

## `impact_unbounded`

A second refusal has the same shape and a different reason. It happens when a limit applies to the action and the call gives no evaluable amount for that limit's dimension.

For a declared action that means the amount expression reads an input the caller left out. `count(input.to)` with no `to` has nothing to count. No quantity is unbounded, never zero.

```json
{
  "stage": "rejected",
  "outcome": "not_dispatched",
  "rejectionReason": "impact_unbounded",
  "grade": "refused",
  "attempts": []
}
```

The invocation comes back `status: "failed"` with an error that names `impact_unbounded`. The error text has this form:

```text
Limit <limit id> (<limit name>) caps <dimension>, and this operation gives no exact quantity or upper bound for it.
```

The limit is untouched:

```json
{ "consumed": 0, "reserved": 0 }
```

Two details from the same test run:

- **With no applicable limit the call runs.** Before the `emails` limit existed, the same input succeeded and the effect recorded an empty `impact`. A limit in another unit did not concern it.
- **The same action with a countable input runs under the same limit.** A call with one recipient succeeded and consumed 1.

The fix is in the definition. Mark the input `required` in the action's schema when the amount depends on it.

## Held allowance

`available` is `limit - consumed - reserved`. `reserved` includes every effect whose outcome is still `pending` or `unknown`. That capacity stays held until evidence or an operator settles it. A period boundary does not release it.

In a test run a refund of 8000 against a daily limit of 10000 lost its response. The reservation was then dated to the previous day. On the new day the limit read:

```json
{ "consumed": 0, "reserved": 8000, "unknownExposure": 8000, "available": 2000 }
```

A new call for 3000 was refused with `impact_limit_exceeded`. After reconciliation found the refund, the 8000 settled into the day it was reserved in and the current day read:

```json
{ "consumed": 0, "reserved": 0, "available": 10000 }
```

So a limit can refuse a call while `consumed` is zero. Look at `reserved` and `unknownExposure`. `unknownExposure` is the part of `reserved` whose outcome is unknown or possibly partial. It is not an additional amount.

## A limit that fills after review

A limit is checked at dispatch, not at review. An approval does not hold capacity. If the limit fills while an invocation waits for its approval, approving it dispatches nothing.

```json
{ "rejectionReason": "impact_limit_exceeded" }
```

In a test run the preview showed `available: 10000` and `sufficient: true` at review. Another refund of 5000 ran before the approval. The approval call succeeded and its `invocation.status` was `failed`. The `allowances` in a preview are as of review.

## What an operator can do

**Raise the limit.** This needs `impact-limits:write`. No key receives that scope by default, so the agent a limit constrains cannot raise it.

```typescript TypeScript
await operator.impactLimits.update('lim_01JABCDEF', { amount: 10 });
```

```python Python
operator.impact_limits.update("lim_01JABCDEF", amount=10)
```

```bash curl
curl -s -X PATCH https://api.anlyon.com/api/v2/impact-limits/lim_01JABCDEF \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 10 }'
```

A limit's name and amount can change. Its dimension, scope and period are fixed. Lowering a limit never releases existing exposure. It blocks new work until availability recovers.

**Wait for the period.** A `day` limit is the UTC day and a `month` limit is the UTC month. `consumed` starts again at zero in the next period. A `total` limit has one period and does not reset. Held exposure carries over, as shown above.

**Resolve held exposure.** If `reserved` is what fills the limit, settle the effects behind it. List them with `effects({ unresolved: true })`, then reconcile or resolve each one. See [When the outcome is unknown](/execution/when-the-outcome-is-unknown).

**Then invoke again.** A refused effect is final. It does not run later when room appears. Send a new invocation with a new `Idempotency-Key`. The same `operationKey` with the same request returns the refused effect, so use a new one of those as well.

## How it behaves

- **Governed means an adapter or a declaration.** A governed action draws from the limit. Keep the provider credential in the vault so the action is the path to the provider.
- **A limit belongs to one environment and scope.** Each environment has its own.
- **The money examples on this page are `stripe.refund`.**
- **The amount of a declared action is the expression you wrote.** For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. Set an `upper_bound` at the most one call can change.
- **A limit bounds quantity.** Five emails fit a limit of five, whoever receives them.
- **An operator holding `impact-limits:write` can raise, lower or archive a limit.** Grant that scope with care.
- **The refusal record is a database record.**
