{"record":{"id":"64a0be736f92d840","repo":"koala73/worldmonitor","slug":"checkout-rate-limited","errorCode":"CHECKOUT_RATE_LIMITED","errorMessage":"Checkout is temporarily rate limited. Retry shortly.","messagePattern":"Checkout is temporarily rate limited\\. Retry shortly\\.","errorType":"exception","errorClass":"ConvexError","httpStatus":429,"severity":"warning","filePath":"convex/payments/checkout.ts","lineNumber":388,"sourceCode":"    if (pending) {\n      throw new ConvexError(buildPendingBlockedPayload(pending));\n    }\n\n    const customerName = identity\n      ? [identity.givenName, identity.familyName].filter(Boolean).join(\" \") ||\n        identity.name\n      : undefined;\n\n    const result = await _createCheckoutSession(args, {\n      userId,\n      email: identity?.email,\n      name: customerName,\n    });\n    // The public Convex action historically rejects provider failures. Keep\n    // that error-channel contract: only the trusted internal relay consumes\n    // the typed outcome and translates it into HTTP 429 + Retry-After.\n    if (isCheckoutRateLimitedOutcome(result)) {\n      throw new ConvexError({\n        code: CHECKOUT_RATE_LIMITED,\n        message: \"Checkout is temporarily rate limited. Retry shortly.\",\n        retryAfterSeconds: result.retryAfterSeconds,\n      });\n    }\n    return result;\n  },\n});\n\n// ---------------------------------------------------------------------------\n// Internal action: called by /relay/create-checkout with trusted userId\n// ---------------------------------------------------------------------------\n\nexport const internalCreateCheckout = internalAction({\n  args: {\n    userId: v.string(),\n    email: v.optional(v.string()),\n    name: v.optional(v.string()),","sourceCodeStart":370,"sourceCodeEnd":406,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/checkout.ts#L370-L406","documentation":"After bounded retry (`CHECKOUT_RATE_LIMIT_MAX_ATTEMPTS`), if Dodo still rate-limits checkout creation, the typed rate-limited outcome is converted into a ConvexError carrying `CHECKOUT_RATE_LIMITED`, a message, and `retryAfterSeconds`. The public action rejects here (the internal relay instead translates the same outcome into HTTP 429 + Retry-After for the trusted gateway).","triggerScenarios":"Dodo returns 429 on checkout creation and the bounded retry loop exhausts its attempts; the result is detected via `isCheckoutRateLimitedOutcome`.","commonSituations":"Burst of checkout attempts from one user/IP; Dodo-side throttling; retry storm from clients that ignore Retry-After.","solutions":["Retry after the `retryAfterSeconds` value carried in the error data","Apply client-side backoff and avoid hammering the checkout endpoint","If the rate limit persists well beyond retryAfter, check Dodo status or contact support"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await convex.action(api.payments.checkout.createCheckout, args);\n} catch (err) {\n  if (err.data?.code === 'CHECKOUT_RATE_LIMITED') {\n    const secs = err.data.retryAfterSeconds ?? 30;\n    // schedule retry after secs; surface 'temporarily rate limited' to user\n  } else { throw err; }\n}","preventionTips":["Honor retryAfterSeconds from the error data","Apply client-side backoff to avoid retry storms","Debounce repeated checkout button clicks"],"tags":["payments","checkout","rate-limit","dodo","retry"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}