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

  1. Revoke an existing key via revokeApiKey before creating a new one.
  2. Call listApiKeys to inspect active keys and identify which to revoke.
  3. If the limit seems wrong, check that revokedAt is set correctly on keys you believe are revoked; the cap counts only non-revoked rows.
  4. 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

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.