All guides
Updated
Reading
9 min
By
Anlyon Team

Per-user limits for AI agents: one rule, counted for each user

Cap what your AI agent does in each user's account: one limit with scope end_user, reserved before each call. One user hitting it never blocks the others.

  • end users
  • per-user limits
  • impact limits
  • rate limiting
  • hubspot

To limit what an AI agent can do for each user, create one limit with scope: 'end_user': every user gets the whole amount, counted before each call. When one user's agent makes its 21st CRM change of the day, that call is refused before it reaches HubSpot, and every other user still has their 20.

await ops.impactLimits.create({
  name: 'CRM changes per user per day',
  dimension: 'resource_mutations',
  period: 'day',
  amount: 20,
  scope: 'end_user',
});

That is the whole rule. The rest of this guide covers why the usual tools do not do this job, how the count works, and what your agent should do when a user runs out. It assumes your agent already acts in your users' accounts; the end-to-end guide covers connecting users.

Why is a global rate limit the wrong tool?

A rate limit on your agent endpoint, or a token budget on your model provider, is where most teams start. Both count the wrong thing for an agent that writes in your users' accounts.

One noisy user starves the others. A global limit is one pool. If a single user's conversation loops and fires 300 Slack posts, the pool is empty for the 2,000 users who did nothing wrong. You set the limit high to avoid that, and then it stops protecting anyone.

Tokens are not side effects. A token budget caps what the model costs. It says nothing about what the agent changed. A tool call that archives a HubSpot contact costs a few hundred tokens, the same as one that reads it. The thing you want to cap is the change in the user's account.

A request count is not a change count. Retries, reads and refusals all look like requests. A useful cap counts writes, and counts them before they are sent.

ControlWhat it countsShared byStops a write before the provider?
Global rate limit on your endpointRequests to your serverEvery userNo, it never sees the provider call
Model token budgetModel usageEvery user, or one keyNo
Anlyon limit, scope: 'environment'Changes, money or a unit you declareEvery call in the environmentYes
Anlyon limit, scope: 'end_user'Changes, money or a unit you declareEach user separatelyYes

The deeper background on limits that count impact, not requests, is in what is an impact limit for AI agents.

How does a per-user limit count each call?

Every call your agent makes for a user goes through a client your server binds: anlyon.forUser(session.user.id). Anlyon reads the user from that binding, never from the model.

Before dispatch, Anlyon works out what the call changes. Every HubSpot and Slack pack write declares one resource mutation. It then reserves that amount against every limit that applies, for that user, in one transaction. If any applicable limit cannot cover it, nothing is sent. If all of them can, the call goes to the provider and the reservation settles when the outcome is known.

Reserving before the call matters under concurrency. If a user has one change left and their agent fires two tool calls at once, checking a counter and incrementing it later lets both through. A reservation lets one through and refuses the other.

A call made without a user is not charged to any user. It draws from the limit's shared allowance for calls with no user, the same pool an environment-scoped limit would use.

How do you choose scope, dimension and period?

Four fields decide what a limit counts.

FieldValuesUse it for
scope'environment' (default) or 'end_user'Whether everyone shares one amount, or each user gets the whole amount
dimension'resource_mutations', 'money' (with currency, in minor units) or a unit you declare, such as 'emails'What one call costs against the limit
period'day' (UTC), 'month' or 'total'When the count starts again
actionName / resourcePrefixAn action name, or a prefix of the call's resource keyNarrowing the limit to one action or one provider

Use both scopes together. An end_user limit stops one user's agent from doing too much. An environment limit caps the total across your platform, for the day something goes wrong everywhere at once. A call has to fit under both. Choose an agent limit scope walks through the trade-offs.

How do you set 20 CRM changes per user per day on HubSpot?

Without actionName or resourcePrefix, a resource_mutations limit counts every governed write in the environment: HubSpot, Slack, Linear and the rest. To count HubSpot alone, narrow it by resource key. A pack action's resource key is http: followed by the host and path, so the HubSpot host covers every HubSpot write.

// ops holds an operator key with impact-limits:write. Never the agent's key.
import { Client } from '@anlyonhq/sdk';

const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });

await ops.impactLimits.create({
  name: 'HubSpot changes per user per day',
  dimension: 'resource_mutations',
  period: 'day',
  amount: 20,
  scope: 'end_user',
  resourcePrefix: 'http:api.hubapi.com',
});

Creating a limit needs the impact-limits:write scope, which no key gets by default. Give it to an operator key in a deploy script. The agent's key should never hold it, so the agent cannot raise its own ceiling.

How do you set 50 Slack posts per user per day?

Narrow by action name when one action is what you want to count.

await ops.impactLimits.create({
  name: 'Slack posts per user per day',
  dimension: 'resource_mutations',
  period: 'day',
  amount: 50,
  scope: 'end_user',
  actionName: 'slack-post-message',
});

Reactions and message edits are separate actions, so they do not count here. Add a second limit for them if you want one.

What happens on the 21st call?

Say user user_42 has made 20 HubSpot changes today. Their agent asks for a 21st:

const { data } = await anlyon
  .forUser('user_42')
  .actions.invoke('hubspot-update-deal-stage', { dealId: '123', dealstage: 'contractsent' });

console.log(data!.status); // 'failed'
console.log(data!.grade);  // 'refused'
console.log(data!.error);  // 'Dispatch refused (impact_limit_exceeded): Not enough allowance: ...'

What happened, in order:

  1. Anlyon read the user from the client, not from the input.
  2. It computed one resource mutation for the call.
  3. It tried to reserve that against user_42's allowance on each applicable limit. The HubSpot limit had 0 left.
  4. It refused the call. Nothing was sent to HubSpot. The receipt grades refused.

At the same moment, user_7 asks for their 3rd change. Their allowance is untouched by user_42, so their call runs. Tomorrow, at the start of the UTC day, user_42 has 20 again.

How should the agent handle the refusal?

The refusal is a result, not a crash. The agent's job is to tell the user plainly and stop.

With the Vercel AI SDK adapter, the tool returns a structured result the model can read. code is 'impact_limit_exceeded', limits lists each limit that ran short with what the call needed and what was left, and message tells the model nothing was sent and not to retry.

import { anlyonTools } from '@anlyonhq/ai-sdk';

const tools = await anlyonTools({
  client: anlyon.forUser(session.user.id),
  actions: ['hubspot-update-deal-stage', 'slack-post-message'],
});
// A refused call returns, for example:
// { status: 'failed', grade: 'refused', code: 'impact_limit_exceeded',
//   limits: [{ name: 'HubSpot changes per user per day', requested: 1, available: 0 }],
//   message: 'Action "hubspot-update-deal-stage" was refused because a limit was reached ... Nothing was sent. Do not retry it now.' }

Calling the SDK directly, read the same fields with refusalOf from @anlyonhq/ai-sdk:

import { refusalOf } from '@anlyonhq/ai-sdk';

const refusal = refusalOf(data!);
if (refusal?.code === 'impact_limit_exceeded') {
  return `You've reached today's limit for CRM changes. Nothing was changed. It resets at 00:00 UTC.`;
}

Three rules for your system prompt:

  • Say it once, in the user's words. "You've reached today's limit for HubSpot changes" beats an error code.
  • Do not retry, and do not route around it. A limit set on a dimension or a provider counts every action it covers, so a different HubSpot write is refused too. A model that tries five variations is the behaviour the limit exists to stop.
  • Offer what still works. Reads are not governed writes, so the agent can still search deals and draft the change for the user to make by hand.

How do you query a user's usage?

Every governed call made with forUser leaves a receipt with the user's id. List a user's receipts for the action and count today's.

const today = new Date().toISOString().slice(0, 10); // UTC date, same as period 'day'

const { data: receipts } = await anlyon.actions.effects({
  endUserId: 'user_42',
  action: 'hubspot-update-deal-stage',
  limit: 100,
});

const ranToday = (receipts ?? []).filter(
  (r) => r.createdAt.startsWith(today) && ['confirmed', 'acknowledged', 'unknown'].includes(r.grade),
);
console.log(`${ranToday.length} HubSpot stage changes today for user_42`);

Each receipt carries grade, impact and unresolvedExposure, which is true while a pending or unknown outcome still holds capacity. A call whose outcome is unknown stays reserved until it is settled, so a lost response cannot hand the user extra allowance. In the console, the Receipts and Invocations pages filter by end user. Query your agent's actions per user has more queries.

Limits of this approach

  • Governed actions are counted; others are not. Pack writes and actions with an adapter or an impact declaration count. A template action with no declaration has no limit, and a call your code makes directly to a provider is outside Anlyon.
  • The user is whoever your server names. A limit per user is as good as the id you pass to forUser. Take it from your session, never from model output.
  • A limit is per environment. Your staging and production environments each have their own limits and counters.
  • Lowering a limit does not release what is already reserved. Calls in flight still settle against it.

Set a per-user limit in a test environment with an operator key: open the console, or read when the limit is hit in the docs.

Frequently asked questions

How do I rate limit an AI agent's tool calls per user?
Create one impact limit with scope 'end_user' and make the agent's calls with a client bound to the user, such as anlyon.forUser(userId). Each user gets the whole amount, counted and reserved before each call, and one user running out does not affect anyone else.
Why not use a global rate limit for my AI agent?
A global limit is one pool for everyone, so one noisy user can use it up and block every other user. It also counts requests, not what changed in the user's account, which is the thing you want to cap.
Can a token budget limit what my AI agent does in a user's account?
No. A token budget caps model usage. One cheap tool call can still archive a contact or post to a channel, so the cap has to count the side effect itself, before the call is sent.
What happens when a user hits their AI agent limit?
The call is refused before anything reaches the provider, and the receipt grades refused. Other users keep their full allowance, and the agent gets a result telling it not to retry.

Let your agent act for your users, safely.

A few lines of server code, then send a user a connect link. Nothing to host.

$ claude mcp add --transport http anlyon https://api.anlyon.com/mcp