Agent rate limiting#

Per-identity limits on copilot turns, with a global circuit breaker. The counter store is pluggable, which is what makes the limiter correct across several instances.

It guards both agent mounts: POST /_janux/agent (the server loop) and POST /_janux/llm (the one-turn model proxy browser loops use). They share the key and the budget, because they share the bill — protecting one door and leaving the other open just moves the abuse.

import { createRateLimiter, createMemoryCounterStore, createRedisCounterStore } from '@janux/agent';

export default defineAgent({
  harness: {
    identityFor: (request) => request.headers.get('x-user-id') ?? 'anonymous',
    rateLimit: { limit: 20, windowMs: 60_000, globalLimit: 500 },
  },
});

createRateLimiter(config)#

RateLimitConfig Default What it does
limit (required) Requests per identity per window
windowMs (required) Window length in milliseconds
globalLimit off Circuit breaker across all identities — the guard against a bill-shaped incident when one bug hammers the endpoint from many sessions
store createMemoryCounterStore() Where counters live

The returned RateLimiter has one method, allow(identity): Promise<boolean>.

It fails open. If the counter store throws — Redis unreachable, network blip — the request is allowed. Losing the limiter degrades your cost control; failing closed would take your copilot down. Pair the limiter with alerting on store errors so an outage is visible rather than silent.

The store contract#

interface CounterStore {
  incr(key: string, windowMs: number): number | Promise<number>;
}

One method: increment the key inside the current window, return the new count. That's the whole seam a custom backend has to implement.

createMemoryCounterStore(now?)#

Per-process counters, injectable clock for tests. Correct for a single instance — and wrong the moment you run two: each replica would allow the full limit, so N replicas mean N×limit. Fine in dev, not in production behind a load balancer.

createRedisCounterStore(options)#

const store = await createRedisCounterStore({ redis: process.env.REDIS_URL!, keyPrefix: 'janux:rl:' });
RedisCounterOptions Notes
redis An ioredis connection string or an existing client (anything with incr and pexpire) — reuse the client you already have rather than opening a second connection
keyPrefix Namespace for the counter keys

Shared counters mean the limit is the limit, whatever the replica count. INCR + PEXPIRE keeps the window self-expiring, so there's nothing to clean up.

identityFor#

harness.identityFor(request) decides who is being limited: a user id, an API key, a session, an org. Return a stable string. Without it, every caller shares one bucket — usually not what you want, and the reason a limiter can look "broken" when it's actually limiting everyone together.

Related: The agent and your copilot · Agent guardrails · Agent memory & storage