{"record":{"id":"8dce6a07b2168fb9","repo":"hcengineering/platform","slug":"platform-status-workspaceratelimit","errorCode":"platform.status.WorkspaceRateLimit","errorMessage":"WorkspaceRateLimit","messagePattern":"WorkspaceRateLimit","errorType":"exception","errorClass":"PlatformError","httpStatus":null,"severity":"warning","filePath":"server/account/src/operations.ts","lineNumber":926,"sourceCode":"    path += `&navigateUrl=${encodeURIComponent(navigateUrl.trim())}`\n  }\n\n  const front = getFrontUrl(branding)\n  const link = concatLink(front, path)\n  ctx.info(`Created invite link: ${link}`)\n\n  return link\n}\n\nfunction checkRateLimit (email: string, workspaceName: string): void {\n  const now = Date.now()\n  const lastInvites = invitesSend.get(email)\n  if (lastInvites !== undefined) {\n    lastInvites.totalSend++\n    lastInvites.lastSend = now\n    if (lastInvites.totalSend > 5 && now - lastInvites.lastSend < 60 * 1000) {\n      // Less 60 seconds between invites\n      throw new PlatformError(\n        new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace: workspaceName })\n      )\n    }\n    invitesSend.delete(email)\n  } else {\n    invitesSend.set(email, {\n      lastSend: now,\n      totalSend: 1\n    })\n  }\n\n  // We need to cleanup map\n  for (const [k, vv] of invitesSend.entries()) {\n    if (vv.lastSend < now - 60 * 1000) {\n      invitesSend.delete(k)\n    }\n  }\n}","sourceCodeStart":908,"sourceCodeEnd":944,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/operations.ts#L908-L944","documentation":"checkRateLimit enforces a per-email invite throttle: if more than 5 invites have been sent to the same email and less than 60 seconds have elapsed since the tracked last send, it throws platform.status.WorkspaceRateLimit. It protects the mail pipeline from flooding and is invoked by createWorkspace and resendInvite.","triggerScenarios":"Calling resendInvite (or createWorkspace) for the same email more than 5 times within a 60-second window; retry loops on resendInvite that do not back off; multiple concurrent invites to one address from different callers.","commonSituations":"An automated retry script hammering resendInvite on transient mail failures; a user double-clicking a resend button repeatedly; load tests against the invite endpoint; shared service accounts inviting the same address from several jobs.","solutions":["Wait at least 60 seconds before resending an invite to the same email.","Add exponential backoff / jitter to any automated retry loop around resendInvite or createWorkspace.","Debounce the resend action in the UI and disable the button after the first click.","Check the invite-send counter before calling: if an invite was recently sent, surface 'invite already sent' instead of calling again.","If the counter is stale from testing, restart the service (invitesSend is in-memory) and re-test with delays."],"exampleFix":"// before\nfor (let i = 0; i < 10; i++) await ops.resendInvite(ctx, token, params)\n// after\nawait backoffRetry(() => ops.resendInvite(ctx, token, params), { minDelayMs: 60_000, maxAttempts: 6 })","handlingStrategy":"retry","validationCode":"if (lastSentAt[email] && Date.now() - lastSentAt[email] < 60_000 && sendCount[email] > 5) {\n  throw new Error('invite throttle: wait before resending to ' + email)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await ops.resendInvite(ctx, token, params)\n} catch (e) {\n  if (isStatus(e, platform.status.WorkspaceRateLimit)) {\n    await sleep(60_000)\n    return retryWithBackoff(() => ops.resendInvite(ctx, token, params))\n  }\n  throw e\n}","preventionTips":["Debounce resend buttons and disable them for 60 seconds after each send.","Implement exponential backoff with jitter on automated retries.","Track per-email send timestamps client-side and refuse to exceed 5 sends/minute."],"tags":["rate-limit","invite","throttling"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}