toeverything/AFFiNE · error · TooManyRequest

too_many_request

too_many_request

Error message

Too many requests.

What it means

Thrown by the inviteByEmail mutation when the emails array contains more than 512 entries. It caps a single invite batch to keep the per-email processing loop (user lookups/creation, membership checks, seat accounting) bounded. Code too_many_request, HTTP 429; unlike per-email failures, this aborts the whole mutation before any candidate is processed.

Solutions

  1. Split the list into chunks of at most 512 emails and call inviteByEmail per chunk.
  2. Preferably chunk smaller (e.g. 100) so per-email results are easier to review and rate limits are not tripped.
  3. Add a client-side count guard: disable the invite button when emails.length > 512 with a clear message.

Example fix

// before
await inviteByEmail(workspaceId, allEmails); // 2000 emails -> TooManyRequest

// after
const CHUNK = 512;
const results = [];
for (let i = 0; i < allEmails.length; i += CHUNK) {
  results.push(...await inviteByEmail(workspaceId, allEmails.slice(i, i + CHUNK)));
}
Defensive patterns

Strategy: validation

Validate before calling

const MAX_INVITE_BATCH = 512;
function chunk<T>(arr: T[], size: number): T[][] {
  const out: T[][] = [];
  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
  return out;
}
const batches = chunk(emails, MAX_INVITE_BATCH); // never send more than 512 in one call

Type guard

function isTooManyRequestError(e: unknown): boolean {
  const ext = (e as { extensions?: Record<string, unknown> })?.extensions;
  return String(ext?.name ?? '').toLowerCase() === 'too_many_request';
}

Try / catch

try {
  return await inviteByEmail(workspaceId, emails);
} catch (e) {
  if (isTooManyRequestError(e) && emails.length > 512) {
    const out: InviteResult[] = [];
    for (const b of chunk(emails, 512)) out.push(...await inviteByEmail(workspaceId, b));
    return out;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling inviteByEmail with emails.length > 512 — e.g. pasting a large distribution list or syncing an org directory in one request. The check runs after permission and workspace-name checks, so nothing is invited when it fires.

Common situations: Bulk onboarding scripts that send the whole CSV at once; HR/IT teams importing a company-wide list; UI that lets users paste unlimited lines into a textarea bound to the invite mutation.

Understand the failure class

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/fbaff62abfe3be39. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:186

      }));
    }
  }

  @Mutation(() => [InviteResult])
  async inviteMembers(
    @CurrentUser() me: CurrentUser,
    @Context() context: GraphqlContext,
    @Args('workspaceId') workspaceId: string,
    @Args({ name: 'emails', type: () => [String] }) emails: string[]
  ): Promise<InviteResult[]> {
    await this.ac
      .user(me.id)
      .workspace(workspaceId)
      .assert('Workspace.Users.Manage');
    await this.assertWorkspaceNameCanInvite(workspaceId);

    if (emails.length > 512) {
      throw new TooManyRequest();
    }

    const results: InviteResult[] = emails.map(email => ({ email }));
    const candidates: InviteCandidate[] = [];
    const seen = new Set<string>();
    for (const [index, email] of emails.entries()) {
      try {
        const normalizedEmail = email.trim().toLowerCase();
        validators.assertValidEmail(normalizedEmail);
        if (seen.has(normalizedEmail)) {
          throw new ActionForbidden('Duplicate invite email.');
        }
        seen.add(normalizedEmail);

        const target = await this.models.user.getUserByEmail(normalizedEmail);
        if (target) {
          const originRecord = await this.models.workspaceUser.get(
            workspaceId,

View on GitHub (pinned to 2af30773ae)