Skip to content

Rate limits

The Auradonors API enforces rate limits on a small set of endpoints today, all using fixed-window counters from Microsoft.AspNetCore.RateLimiting.

PolicyLimitWindowWhere it’s attached
contact-form5 requests15 minutesPOST /api/contact-form (the public marketing form). Limited per source IP.
mfa-verify5 attempts15 minutesMFA verify + recovery-code endpoints. Limited per user-id (or source IP if unauthenticated). Cookie-auth flow only — not relevant to API key callers.
mfa-send-email3 sends1 hourThe MFA email-OTP send endpoint. Limited per user-id. Cookie-auth flow only.

API-key authenticated requests under /api/tenants/... are not rate-limited at the application layer today. Edge and infrastructure limits at the platform layer (App Platform’s network-level shaping) still apply. Future versions of the API are expected to add per-key fairness; plan for it by implementing the retry strategy below regardless.

When the limiter rejects a request:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "message": "Too many requests. Please try again later." }

The response is a flat JSON object today, not an RFC 7807 ProblemDetails envelope. A future revision is expected to align it with the rest of the error model. Treat the body as opaque and switch on response.statusCode == 429.

The platform does not currently emit a Retry-After header. See the backoff guidance below for recommended pacing.

For automated integrations:

  1. Don’t retry immediately. A 429 means the server has measured that you’re over budget; an immediate retry will burn through the budget the moment the window resets and trip again.
  2. Back off exponentially with jitter. Start at one minute, double on each consecutive 429, cap at fifteen minutes. Add ±20 % jitter so a restart of multiple workers doesn’t hammer the same window.
  3. Surface persistent 429s as alerts. Three or more consecutive 429s on the same logical operation indicates a sizing problem — your integration is doing more than the policy allows.
async Task<HttpResponseMessage> SendWithBackoffAsync(HttpRequestMessage request)
{
var delay = TimeSpan.FromMinutes(1);
while (true)
{
var response = await http.SendAsync(request);
if ((int)response.StatusCode != 429) return response;
// ±20% jitter so synchronized clients don't all retry at the same instant.
var jitter = Random.Shared.NextDouble() * 0.4 - 0.2;
await Task.Delay(delay * (1 + jitter));
delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, TimeSpan.FromMinutes(15).Ticks));
}
}

For interactive callers (a person waiting on a UI), surface a “try again in a moment” message instead of retrying silently.

Until per-key rate limits land, design for fairness anyway:

  • Batch where you can. Most list endpoints accept pageSize up to reasonable upper bounds (see Pagination).
  • Cache reference data. Funds, departments, batch sources, contact sources, and tag categories don’t change often; pull them once per process start.
  • Use webhooks rather than poll loops. When a downstream change triggers your work, Webhooks is the lower-impact path.
  • Avoid synchronous fan-out from a single user action. A donor click shouldn’t translate to a hundred outbound calls.
  • No per-key throttle on /api/tenants/.... The api-key policy defined in Program.cs is wired but not attached to any endpoint group; planned for a future revision.
  • No Retry-After header. Use the backoff guidance above.
  • No usage / quota dashboard. No view of your current rate-limit state. Treat the policies as soft constraints and design accordingly.
  • No X-RateLimit-* advisory headers. Future versions may add them alongside the per-key throttle.