Skip to content
bits
Ship an agent to the edge · Lesson 5 of 8

Limits that hold

Tutor:

An agent is a loop that spends money each time round. On your laptop, the worst case was your own bill. On the internet, the worst case is a stranger with a script, or one confused conversation that calls a tool forever.

No single limit covers that. You stack four, from cheapest to check to most important:

Per request: a rate limit stops bursts before any work happens.

Per run: a turn cap and max_tokens bound one question.

Per user, per day: a token budget bounds one person.

Per account: an AI Gateway spend limit is the backstop if your code has a bug.

Tutor:

The first one is a Workers binding. You declare it in config and call it with a key, ideally the user id, not the IP address (many people can share an IP):

{
"ratelimits": [
{
"name": "ASK_LIMIT",
"namespace_id": "1001",
"simple": { "limit": 10, "period": 60 }
}
]
}
JSONUTF-8
Tutor:

period must be 10 or 60 seconds. And this limiter is approximate: counts are kept per Cloudflare location and synced in the background, so a burst can slip past it. Good for bursts. Not a way to cap spending.

quick check+20 neurons
Tutor:

Why isn't the rate-limit binding enough to cap what a user can spend?

    Type your answer or tap an option · Enter checks

    Tutor:

    For the exact count, you already have the right place: the user's own agent. It's one instance per user with its own database, so every turn that user takes is counted in one spot. Here's the loop from Your first agent loop with a turn cap and a daily budget added:

    const MAX_TURNS = 6;
    const DAILY_TOKENS = 200_000; // ≈ $2 a day at most on Sonnet 5.5, even if all output
    const today = () => new Date().toISOString().slice(0, 10); // UTC date
    for (let turn = 1; turn <= MAX_TURNS; turn++) {
    if (this.tokensToday() >= DAILY_TOKENS) {
    return "You've reached today's limit. It resets at midnight UTC.";
    }
    const res = await client.messages.create({ model, max_tokens: 4096, tools, messages });
    this.addTokens(res.usage.input_tokens + res.usage.output_tokens);
    this.save({ role: "assistant", content: res.content });
    if (res.stop_reason !== "tool_use") return textOf(res);
    this.save({ role: "user", content: await runTools(res) });
    }
    return "I stopped after 6 steps without an answer. Try a narrower question.";
    // in the class:
    tokensToday(): number {
    const [row] = this.sql<{ tokens: number }>`SELECT tokens FROM usage WHERE day = ${today()}`;
    return row?.tokens ?? 0;
    }
    addTokens(n: number) {
    this.sql`INSERT INTO usage (day, tokens) VALUES (${today()}, ${n})
    ON CONFLICT(day) DO UPDATE SET tokens = tokens + ${n}`;
    }
    TypeScriptUTF-8● lines 5–16: check before, count after
    Tutor:

    Check before each call, count after: you only learn the cost when the response arrives. A request already in flight can overshoot by one turn, which is why max_tokens caps each turn too. When the loop gives up, it says so plainly instead of failing silently.

    Finally, set a spend limit in AI Gateway: a dollar budget per day, split by the user_id you tagged in the last lesson. It's eventually consistent too, and it only sees calls that go through the gateway, but it catches what your code misses.

    quick check+20 neurons
    Tutor:

    The loop hits MAX_TURNS with the model still asking for tools. What's the best thing to do?

      Type your answer or tap an option · Enter checks