Mail rate limit (defaults, errors, log events)
Feiten over de Microsoft Graph mail rate limiter: defaults, override-velden, Redis-keys, errors en log-events.
Voor begrip en ontwerp, zie de explanation.
Defaults
Bron: apps/api/src/common/services/mail-provider-service/rate-limit/mailRateLimitConfig.ts.
| Naam | Default | Toelichting |
|---|---|---|
perMinute |
25 |
Maximaal aantal sendMail-calls per minuut |
recipientsPerDay |
9000 |
Maximaal aantal ontvangers per 24 uur |
De defaults staan iets onder de Microsoft Graph-limieten (~30/min en 10.000/dag) als veiligheidsmarge.
Override-velden per systeem-mailbox
Velden op het SystemMailbox-record (zie apps/api/src/api/system-mailbox/SystemMailbox.interface.ts):
| Veld | Type | Default | Toelichting |
|---|---|---|---|
rateLimitPerMinute |
number | leeg | Override op perMinute. Positief geheel getal. |
rateLimitRecipientsPerDay |
number | leeg | Override op recipientsPerDay. Positief geheel getal. |
Een leeg of niet-positief veld valt terug op de default. Wijzigen via PUT /admin/system-mailboxes/:key invalideert de factory-cache, dus geen pod-restart nodig.
Tenant-Graph-mailers (provider: 'microsoft-graph' in de tenant-mail-setting) hebben vandaag geen override en draaien altijd op de defaults.
Welke mailers worden gewrapt
apps/api/src/common/services/mail-provider-service/MailerFactory.ts:
| Pad | Wordt gewrapt door rate limiter? |
|---|---|
Tenant-mailer, provider: 'microsoft-graph' |
Ja |
Tenant-mailer, provider: 'sendgrid' of default |
Nee |
| Systeem-mailbox (altijd via Microsoft Graph) | Ja |
mailboxKey in de limiter is het volledige mailadres, genormaliseerd via trim().toLowerCase().
Redis-keys
Bron: apps/api/src/common/services/mail-provider-service/rate-limit/MailRateLimiter.ts.
| Patroon | Bevat | TTL |
|---|---|---|
mail:rate:min:<mailboxKey>:<minuteBucket> |
Aantal sendMail-calls | 120 seconden |
mail:rate:day:<mailboxKey>:<dayBucket> |
Aantal ontvangers | 26 uur |
minuteBucket = floor(epochSeconds / 60), dayBucket = floor(epochSeconds / 86400). Buckets rollen vanzelf over.
Reservering en release gebeuren via twee server-side Lua-scripts (mailRateCheckAndReserve, mailRateRelease). Beide worden bij eerste gebruik per Redis-verbinding geregistreerd via defineCommand.
Reservering-flow
RateLimitedGraphMailer.send (apps/api/src/common/services/mail-provider-service/rate-limit/RateLimitedGraphMailer.ts):
- Tel
recipientCountuitmessage.recipients.length. - Roep
limiter.checkAndReserve({ mailboxKey, recipientCount, limits })aan. - Geweigerd → throw
MailRateLimitExceededError(mailboxKey, reason, retryAfterMs). - Toegestaan → roep
inner.send(message)aan. - Inner gooit →
limiter.release(...)om de reservering terug te draaien. - Inner slaagt → reservering blijft staan.
Reschedule-flow
MailProviderService.send catch-pad voor MailRateLimitExceededError (apps/api/src/common/services/mail-provider-service/MailProviderService.ts):
const spreadMs = computeSpreadMs(message.id); // 0..29999
const newScheduledOn = new Date(Date.now() + error.retryAfterMs + spreadMs);
await messageService.update(message.id, {
status: MessageStatusEnum.SCHEDULED,
scheduledOn: newScheduledOn,
});
computeSpreadMs is FNV-1a hash van message.id modulo 30.000 ms. Deterministisch per id.
De BullMQ-worker send-scheduled-messages pakt de message vanzelf opnieuw op zodra scheduledOn voorbij is. Geen aparte retry-queue.
Errors
MailRateLimitExceededError (apps/api/src/common/services/mail-provider-service/rate-limit/MailRateLimitExceededError.ts):
| Property | Type | Toelichting |
|---|---|---|
name |
'MailRateLimitExceededError' |
Vaste naam |
mailboxKey |
string | Het mailbox-adres dat de limiet raakte |
reason |
'minute' \| 'day' |
Welke limiet werd geraakt |
retryAfterMs |
number | Geschatte ms tot het bucket-window opnieuw vrij is |
message |
string | 'Mail rate limit exceeded for X (reason); retry in Yms' |
retryAfterMs is gebaseerd op het resterende deel van het minuut- of dag-window, niet op een server-side advies. Het is dus een minimum, niet een belofte.
Log-events
| Event | Niveau | Velden | Wanneer |
|---|---|---|---|
mail.rateLimit.deferred |
info | mailboxKey, messageUuid, reason, retryAfterMs, newScheduledOn |
Message wordt opnieuw ingepland na een rate-limit-deny |
mail.rateLimit.redisError |
warn | mailboxKey, err, op? ('release' als release faalt) |
Redis niet bereikbaar of Lua-call gooit. Limiter gaat fail-open. |
Configuratie en singleton
getMailRateLimiter() (mailRateLimiter.singleton.ts) geeft één gedeelde instance per pod, gebaseerd op getRedisConnectionHelper(false). Geen aparte env-vars: er wordt dezelfde Redis-verbinding gebruikt als de rest van de api.
Voor tests: __setMailRateLimiterForTests(custom) injecteert een eigen instance.
Validatie op de admin-API
PUT /admin/system-mailboxes/:key valideert (apps/api/src/api/system-mailbox/systemMailboxRouter.ts):
| Veld | Regel |
|---|---|
rateLimitPerMinute |
Optioneel. Indien gezet: positief geheel getal, anders 400. |
rateLimitRecipientsPerDay |
Optioneel. Indien gezet: positief geheel getal, anders 400. |
Foutboodschappen:
rateLimitPerMinute moet een positief geheel getal zijnrateLimitRecipientsPerDay moet een positief geheel getal zijn
Een veld weglaten in de PUT-body laat de bestaande waarde staan.
Bron-bestanden
- Limiter:
apps/api/src/common/services/mail-provider-service/rate-limit/MailRateLimiter.ts - Decorator:
apps/api/src/common/services/mail-provider-service/rate-limit/RateLimitedGraphMailer.ts - Singleton:
apps/api/src/common/services/mail-provider-service/rate-limit/mailRateLimiter.singleton.ts - Config en
resolveLimits:apps/api/src/common/services/mail-provider-service/rate-limit/mailRateLimitConfig.ts - Error:
apps/api/src/common/services/mail-provider-service/rate-limit/MailRateLimitExceededError.ts - Wrapping in factory:
apps/api/src/common/services/mail-provider-service/MailerFactory.ts - Reschedule-flow:
apps/api/src/common/services/mail-provider-service/MailProviderService.ts - Validatie:
apps/api/src/api/system-mailbox/systemMailboxRouter.ts