SHOPin Logo
Skip to main documentation content

Rate limiting

Optional per-IP rate limiting for the BFF in the SHOPin storefront accelerator. It is off unless you set BFF_RATE_LIMIT_ENABLED=true; it does not run in the presentation app.

For how 429 fits the global error shape and filters, see Error handling.


Configuration

Only the literal string true for BFF_RATE_LIMIT_ENABLED enables throttling. Any other value or an unset variable leaves limits disabled (the guard does not apply caps even if TTL/limit variables are set).

When enabled, set:

VariableMeaning
BFF_RATE_LIMIT_TTLDefault window (milliseconds)
BFF_RATE_LIMIT_LIMITMax requests per IP in that window
BFF_RATE_LIMIT_SECURE_TTLSecure window (milliseconds)
BFF_RATE_LIMIT_SECURE_LIMITMax requests per IP in that window on @SecureRateLimit() routes

Default throttler — Applies to all routes while limiting is on (first two variables).

Secure throttler — Applies only on handlers with @SecureRateLimit() (last two variables). On those routes the client must stay under both limits.

In the reference repo, @SecureRateLimit() is on: the whole auth.controller.ts, POST create order, PUT change customer password, and mutating routes in payment.controller.ts (PUT set method, POST initiate — not GET methods).

OptionalThrottlerGuard is the app guard in app.module.ts; options are built in throttler.config.ts.

Starting points (not prescriptions): e.g. 60000 ms / 100200 per IP for default, 60000 ms / ~5 per IP for secure. Tune for traffic and how client IP is seen behind proxies.

Multiple BFF replicas

The throttler stores counts per BFF process. With more than one replica, the env limits are not a single global budget per client unless traffic is pinned to one instance—turn BFF_RATE_LIMIT_ENABLED off and enforce limits outside the BFF (gateway, proxy, CDN/WAF, or another layer with fleet-wide or shared state). Using the built-in throttler alone in that setup is misleading unless you add shared storage yourself (outside the default accelerator).


429 in the presentation app

The BFF returns 429 with the usual JSON error body; logging follows other client errors (see Error handling).

In the presentation app, use HttpError.getStatusCode(error) === 429 or HttpError.isTooManyRequestsError(error) from error-utils.ts, or handle RateLimitError where the BFF client throws it. Prefer a clear “try again later” message and avoid tight retries or repeated submits that amplify load.

TypeScript
import { HttpError } from '@/lib/error-utils'

export function rateLimitMessage(error: unknown): string | null {
  if (HttpError.isTooManyRequestsError(error)) {
    return 'Too many attempts. Try again in a moment.'
  }
  return null
}

For a full form flow (loading state, field errors, and 429 copy), follow existing auth forms (e.g. auth-login-form.tsx) rather than only this stub.


Related


Back to Validation & resilience · Back to How to work with SHOPin