Files
triggerdotdev--trigger.dev/apps/webapp/app/services/deploymentRateLimit.server.ts
Saadi Myftija 02de2e693f feat(api): separate rate limit budget for deployment endpoints (#4565)
Most deploy-flow API calls shared the general per-environment rate limit
bucket with all of that environment's runtime traffic, so an org with
heavy API usage could intermittently 429 its own deploys; the
`/api/v*/deployments` endpoints themselves were fully exempt from rate
limits as a stopgap
([#2774](https://github.com/triggerdotdev/trigger.dev/pull/2774)), which
promised a dedicated limiter as the follow-up. This is that follow-up:
the whole deploy-flow group now runs on its own budget, separate from
runtime API limits.

### Design

A new `deploymentRateLimiter` covers every endpoint the deploy flow
depends on: the `/api/v*/deployments` group, the env API key exchange
(`/api/v1/projects/:ref/:env`), build-time env var resolution and sync
(`/envvars`, `/envvars/:slug/import`), preview branches,
`/api/v1/remote-build-provider-status` and `/api/v1/artifacts`. The
general API limiter whitelists the same shared path list, so exactly one
limiter applies to each path and the two can't drift apart.

Buckets are keyed per environment for environment API keys and per token
for the PAT-authenticated phase of a CLI deploy (whoami, key exchange,
branches). The deploy budget is controlled via the
`DEPLOYMENT_RATE_LIMIT_*` env vars.
2026-08-11 17:51:31 +02:00

54 lines
1.8 KiB
TypeScript

import { env } from "~/env.server";
import { resolvePrivateApiKeyRateLimitScope } from "~/models/runtimeEnvironment.server";
import { authorizationRateLimitMiddleware } from "./authorizationRateLimitMiddleware.server";
import { deploymentApiPaths } from "./deploymentApiPaths.server";
import type { Duration } from "./rateLimiter.server";
export const deploymentRateLimiter = authorizationRateLimitMiddleware({
redis: {
port: env.RATE_LIMIT_REDIS_PORT,
host: env.RATE_LIMIT_REDIS_HOST,
username: env.RATE_LIMIT_REDIS_USERNAME,
password: env.RATE_LIMIT_REDIS_PASSWORD,
tlsDisabled: env.RATE_LIMIT_REDIS_TLS_DISABLED === "true",
clusterMode: env.RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED === "1",
},
keyPrefix: "deployment",
defaultLimiter: {
type: "tokenBucket",
refillRate: env.DEPLOYMENT_RATE_LIMIT_REFILL_RATE,
interval: env.DEPLOYMENT_RATE_LIMIT_REFILL_INTERVAL as Duration,
maxTokens: env.DEPLOYMENT_RATE_LIMIT_MAX,
},
limiterCache: {
fresh: 60_000 * 10,
stale: 60_000 * 20,
maxItems: 1000,
},
limiterConfigOverride: async (authorizationValue) => {
const rawApiKey = authorizationValue.replace(/^Bearer /, "");
if (!rawApiKey.startsWith("tr_")) {
return;
}
const scope = await resolvePrivateApiKeyRateLimitScope(rawApiKey);
if (!scope) {
return;
}
// Identifier only: the org's apiRateLimiterConfig governs the general API
// limiter, not the deploy budget.
return {
identifier: scope.environmentId,
};
},
pathMatchers: deploymentApiPaths,
log: {
rejections: env.DEPLOYMENT_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
requests: env.DEPLOYMENT_RATE_LIMIT_REQUEST_LOGS_ENABLED === "1",
limiter: env.DEPLOYMENT_RATE_LIMIT_LIMITER_LOGS_ENABLED === "1",
},
});