Appearance
Rate Limiting
Rate limiting in Microtec is enforced at two layers: the Azure Front Door WAF (global tier, IP-based) and the Ocelot API Gateway (per-client, per-route tier). This two-tier approach prevents both volumetric attacks and misbehaving API clients from degrading service for other tenants.
Two-Tier Architecture
| Tier | Enforcer | Scope | Limit |
|---|---|---|---|
| 1 — Network | Azure Front Door WAF | Per source IP | 10,000 requests / 5 minutes |
| 2 — Application | Ocelot API Gateway | Per client token / tenant | Configurable per route |
Ocelot Rate Limiter Configuration
Ocelot's built-in rate limiting middleware is configured in Platforms/Src/Gateway/ocelot.{env}.json:
Global Defaults
json
{
"GlobalConfiguration": {
"RateLimitOptions": {
"ClientWhitelist": ["internal-services"],
"EnableRateLimiting": true,
"Period": "1m",
"PeriodTimespan": 60,
"Limit": 200,
"HttpStatusCode": 429,
"QuotaExceededMessage": "API rate limit exceeded. Please retry after {0} seconds.",
"RateLimitCounterPrefix": "ocelot"
}
}
}The ClientWhitelist entry "internal-services" exempts requests with a valid XApiKey header from rate limiting. Internal microservice calls are never rate-limited.
Per-Route Overrides
Compute-intensive routes have tighter limits:
json
{
"Routes": [
{
"UpstreamPathTemplate": "/api/v1/invoices/export",
"UpstreamHttpMethod": ["GET"],
"DownstreamPathTemplate": "/api/v1/invoices/export",
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "1m",
"Limit": 5,
"PeriodTimespan": 60
}
},
{
"UpstreamPathTemplate": "/api/v1/reports/{everything}",
"UpstreamHttpMethod": ["GET"],
"DownstreamPathTemplate": "/api/v1/reports/{everything}",
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "1m",
"Limit": 10,
"PeriodTimespan": 60
}
}
]
}Per-Client Limits
Ocelot identifies clients by the ClientId header (injected by the Angular SPA from the Keycloak clientId claim). Different client types have different quotas:
| Client Type | Period | Limit | Use Case |
|---|---|---|---|
| SPA (Angular) | 1 minute | 200 requests | Normal user browsing |
| Mobile app | 1 minute | 100 requests | Mobile ERP clients |
| Integration (API key) | 1 minute | 500 requests | Third-party integrations |
| Export/Report endpoints | 1 minute | 5 requests | Heavy computation |
| Internal services (XApiKey) | Unlimited | — | Whitelisted |
429 Response Format
When the rate limit is exceeded, the Gateway returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-Rate-Limit-Limit: 200
X-Rate-Limit-Remaining: 0
X-Rate-Limit-Reset: 1705313460
Content-Type: application/json
{
"success": false,
"data": null,
"message": "API rate limit exceeded. Please retry after 42 seconds.",
"errors": null
}| Header | Description |
|---|---|
Retry-After | Seconds until the rate limit window resets |
X-Rate-Limit-Limit | Total requests allowed in the window |
X-Rate-Limit-Remaining | Requests remaining in the current window |
X-Rate-Limit-Reset | Unix timestamp when the window resets |
Angular Client Handling
The Angular shared library handles 429 responses with an automatic retry-after delay:
typescript
// libs/shared-lib/src/lib/interceptors/rate-limit.interceptor.ts
@Injectable()
export class RateLimitInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 429) {
const retryAfter = parseInt(error.headers.get('Retry-After') ?? '60', 10);
return timer(retryAfter * 1000).pipe(
switchMap(() => next.handle(req))
);
}
return throwError(() => error);
})
);
}
}Automatic Retry for User-Initiated Actions
The interceptor only auto-retries requests triggered by user actions (button clicks, navigation). Background polling requests (e.g. notification count refresh) are NOT auto-retried — they are dropped and logged. Auto-retrying background requests would undermine the rate limit.
AFD WAF Rate Limit
The Azure Front Door WAF custom rule limits requests per source IP:
Rule name: RateLimit-Global
Match condition: All requests (no path filter)
Rate limit threshold: 10,000 requests
Rate limit duration: 5 minutes
Action: Block (returns 429)This protects against volumetric DDoS and prevents a single source IP from overwhelming all tenants. The threshold is set high enough to not affect legitimate power users but low enough to block automated attack tools.
Monitoring Rate Limit Events
Rate limit hits are logged to Seq and Application Insights:
csharp
// Gateway logs rate limit events via Serilog
Log.Warning("Rate limit exceeded for client {ClientId} on route {Route}. " +
"Limit: {Limit}, Period: {Period}",
clientId, route, limit, period);In Application Insights, query for rate limit events:
kusto
requests
| where resultCode == "429"
| summarize count() by bin(timestamp, 5m), name
| order by count_ descAlert threshold: if 429 rate exceeds 5% of total requests for any 5-minute window, the DevOps team is notified to investigate whether a single client is misbehaving or whether limits need adjustment.