Rate limits
The Auradonors API enforces rate limits on a small set of endpoints today,
all using fixed-window counters from
Microsoft.AspNetCore.RateLimiting.
Active policies
Section titled “Active policies”| Policy | Limit | Window | Where it’s attached |
|---|---|---|---|
contact-form | 5 requests | 15 minutes | POST /api/contact-form (the public marketing form). Limited per source IP. |
mfa-verify | 5 attempts | 15 minutes | MFA 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-email | 3 sends | 1 hour | The 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.
429 response shape
Section titled “429 response shape”When the limiter rejects a request:
HTTP/1.1 429 Too Many RequestsContent-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.
Recommended retry strategy
Section titled “Recommended retry strategy”For automated integrations:
- Don’t retry immediately. A
429means 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. - 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. - Surface persistent
429s as alerts. Three or more consecutive429s 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.
Designing within the budget
Section titled “Designing within the budget”Until per-key rate limits land, design for fairness anyway:
- Batch where you can. Most list endpoints accept
pageSizeup 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.
Deliberate non-goals (today)
Section titled “Deliberate non-goals (today)”- No per-key throttle on
/api/tenants/.... Theapi-keypolicy defined inProgram.csis wired but not attached to any endpoint group; planned for a future revision. - No
Retry-Afterheader. 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.