koala73/worldmonitor · error · ConvexError
KEY_LIMIT_REACHED
KEY_LIMIT_REACHED
Error message
KEY_LIMIT_REACHED
What it means
Thrown by createApiKey when, after auto-revoking overflow keys to converge toward the cap, the active key count is still >= MAX_KEYS_PER_USER (5). The mutation first tries to self-heal by revoking the oldest overflow rows to free a slot; this error means convergence did not yield a free slot (e.g. the overflow-revocation path did not reduce the count below the cap). It is an intentional reject-at-cap policy rather than silent rotation of a valid key.
Source
Thrown at convex/apiKeys.ts:109
// revoking enough oldest overflow rows to make room for this create.
const existing = await ctx.db
.query("userApiKeys")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.collect();
const active = existing.filter((k) => !k.revokedAt);
let activeCount = active.length;
if (active.length > MAX_KEYS_PER_USER) {
active.sort((a, b) => a.createdAt - b.createdAt);
const toRevoke = active.slice(0, active.length - (MAX_KEYS_PER_USER - 1));
const now = Date.now();
for (const key of toRevoke) {
await ctx.db.patch(key._id, { revokedAt: now });
}
// After revoking overflow keys there is always exactly one slot free.
activeCount = MAX_KEYS_PER_USER - 1;
}
if (activeCount >= MAX_KEYS_PER_USER) {
throw new ConvexError("KEY_LIMIT_REACHED");
}
// Guard against duplicate hash (astronomically unlikely, but belt-and-suspenders)
const dup = await ctx.db
.query("userApiKeys")
.withIndex("by_keyHash", (q) => q.eq("keyHash", args.keyHash))
.first();
if (dup) {
throw new ConvexError("DUPLICATE_KEY");
}
const id = await ctx.db.insert("userApiKeys", {
userId,
name: args.name.trim(),
keyPrefix: args.keyPrefix,
keyHash: args.keyHash,
scopes,
companyMonitoringAccountId: companyMonitoringAccount?.logicalAccountId,View on GitHub (pinned to ffec79ac33)
Solutions
- Revoke an existing key via revokeApiKey before creating a new one.
- Call listApiKeys to inspect active keys and identify which to revoke.
- If the limit seems wrong, check that revokedAt is set correctly on keys you believe are revoked; the cap counts only non-revoked rows.
- Request raising MAX_KEYS_PER_USER if the use case genuinely needs more concurrent keys.
Example fix
// before — creating a 6th key
await createApiKey(ctx, { name, keyPrefix, keyHash }); // throws KEY_LIMIT_REACHED
// after — revoke oldest first
const keys = await listApiKeys(ctx, {});
const oldest = keys.filter(k => !k.revokedAt).sort((a,b) => a.createdAt - b.createdAt)[0];
if (oldest) await revokeApiKey(ctx, { keyId: oldest.id });
await createApiKey(ctx, { name, keyPrefix, keyHash }); Defensive patterns
Strategy: validation
Validate before calling
const keys = await listApiKeys(ctx, {});
const activeCount = keys.filter(k => !k.revokedAt).length;
if (activeCount >= 5) {
// revoke oldest active before creating
const oldest = keys.filter(k => !k.revokedAt).sort((a,b) => a.createdAt - b.createdAt)[0];
if (oldest) await revokeApiKey(ctx, { keyId: oldest.id });
}
await createApiKey(ctx, { name, keyPrefix, keyHash }); Try / catch
try {
await createApiKey(ctx, { name, keyPrefix, keyHash });
} catch (e) {
if (e instanceof ConvexError && e.message === "KEY_LIMIT_REACHED") {
// prompt user to revoke a key, then retry
} else throw e;
} Prevention
- Track active key count in the UI and disable Create when at the cap.
- Prompt the user to revoke before creating when near the limit.
- Be aware the server self-heals overflow but still enforces a hard cap.
When it happens
Trigger: A user already has 5 or more active (non-revoked) API keys and calls createApiKey again; a prior concurrent race created more than MAX_KEYS_PER_USER rows and the convergence slice did not free a slot (the overflow math yields activeCount >= MAX_KEYS_PER_USER after the patch loop).
Common situations: Long-lived users who accumulated the maximum keys; concurrent createApiKey calls in a race that both passed the limit check; revokedAt values missing due to a partial write, so rows count as active unexpectedly.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/a7ece7fed9407ec0.
Report an issue: GitHub.