Rate Limits

Purpose and Scope

Rate limits are part of Dub's API contract: they tell API consumers how much traffic a key or workspace can send before Dub asks the client to slow down. In the public API documentation, Dub presents this behavior using standard rate-limit response headers, including X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After, and explains that clients should expect a 429 Too Many Requests response when a limit is exceeded. In the repository, the reusable implementation point for those headers is the authentication utility rateLimitRequest, which wraps the shared Upstash-backed limiter and returns both the success flag and response header values that routes can attach to their responses.

Sources: apps/web/lib/auth/rate-limit-request.ts

This page focuses on how to read and extend Dub's rate-limit behavior from the codebase. It covers three related but distinct ideas. First, request rate limiting protects API routes from excessive traffic by an identifier such as an API key, token, or other route-defined key. Second, plan or trial limits constrain business resources such as links, tracked events, API calls, and analytics API calls. Third, integration-specific throttling can defer work when an upstream provider, such as Bitly, reports that its own quota has been reached. Treat these as complementary controls rather than one single global mechanism.

Sources: apps/web/lib/auth/rate-limit-request.ts, apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts, packages/utils/src/constants/pricing/trial-limits.ts

Relevant Source Files

  • apps/web/lib/auth/rate-limit-request.ts - Defines the shared rateLimitRequest helper used by server code to enforce a request budget for an identifier and return rate-limit response headers.
  • apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts - Implements a Bitly import guard that checks Bitly platform limits and requeues the import with a delay when the upstream limit has been reached.
  • packages/utils/src/constants/pricing/trial-limits.ts - Defines trial-period resource caps, including API and analytics API limits, and applies them over normal plan limits while a workspace subscription is trialing.

Public API Contract

Dub's public rate-limit contract is header-oriented. Clients should inspect X-RateLimit-Limit for the size of the current request budget, X-RateLimit-Remaining for the number of requests still available in the current window, and X-RateLimit-Reset for the reset time. The documentation also lists Retry-After, which tells clients how long to wait before trying again after a limit is hit. The source helper returns all four names in one headers object, which keeps the contract centralized and reduces the chance that individual routes format the same values inconsistently.

Sources: apps/web/lib/auth/rate-limit-request.ts

The helper accepts an identifier, a requests count, and an interval. The interval type is intentionally constrained to string forms such as "60 s" or "1 m", which makes route-level configuration explicit: callers choose both the number of requests and the size of the window when they invoke the helper. Internally, the helper calls ratelimit(requests, interval).limit(identifier) and destructures success, limit, reset, and remaining. It then converts the numeric values to strings for HTTP headers, matching the way response metadata is normally serialized.

Sources: apps/web/lib/auth/rate-limit-request.ts

A successful limiter check does not itself send a response. Instead, rateLimitRequest returns a small decision object. Route handlers or middleware can use success to decide whether to continue processing or return 429 Too Many Requests, and can attach the returned headers in either case so clients can adapt their retry behavior. This design keeps the limiter independent from any single route framework primitive while still making it easy for API endpoints to expose consistent response metadata. When adding a new API family, wire the helper near authentication or authorization so the identifier is already known.

Sources: apps/web/lib/auth/rate-limit-request.ts

System-to-Code Mapping

The request-limiter layer lives under apps/web/lib/auth, which signals that API rate limiting is closely related to authenticated access. API keys identify a workspace and determine which resources can be accessed; the limiter identifier is the key used to count requests against a caller-specific budget. The public docs distinguish API keys, which are server-side secrets for REST API access, from publishable keys, which are intended for client-side conversion tracking. Rate limiting should follow that distinction: server-to-server endpoints normally limit by private API key or workspace identity, while tracking flows may use a different identifier appropriate to publishable-key traffic.

Sources: apps/web/lib/auth/rate-limit-request.ts

Plan and trial limits are represented separately in packages/utils/src/constants/pricing/trial-limits.ts. TRIAL_LIMITS includes resource ceilings for links, clicks, payouts, domains, tags, folders, groups, partners, users, ai, api, and analyticsApi. The presence of both api and analyticsApi is important: it means Dub can model general API usage and analytics API usage as separate billable or enforceable resources. Those constants are not HTTP headers by themselves, but they inform the larger product-level limits that route logic, billing UI, or overage messaging can enforce.

Sources: packages/utils/src/constants/pricing/trial-limits.ts

The trial-limit helper getWorkspaceLimitsForStripeSubscriptionStatus shows how temporary billing state changes effective limits. If a workspace is not trialing, the function returns the plan limits unchanged. If it is trialing, the function returns a copy of the plan limits with trial-specific values substituted for the listed resources, including api and analyticsApi. This keeps trial behavior deterministic and prevents unrelated plan configuration from leaking into the trial period. When debugging a rate-limit complaint from a trial workspace, check both route-level request limits and these effective workspace limits.

Sources: packages/utils/src/constants/pricing/trial-limits.ts

Implementation Details

The compact contract for the shared limiter is: rateLimitRequest({ identifier, requests, interval }) returns { success, headers }. identifier is the logical bucket name. requests is the allowed count for the configured window. interval is a typed string ending in seconds or minutes. headers contains Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The source currently sets both Retry-After and X-RateLimit-Reset from the limiter's reset value. Downstream code should preserve those names exactly because clients and generated API documentation expect them.

Sources: apps/web/lib/auth/rate-limit-request.ts

ComponentSource-level nameResponsibility
Shared request limiterrateLimitRequestCounts requests for an identifier over a caller-provided interval and returns standard headers.
Upstash limiter factoryratelimit(requests, interval)Creates the backing limiter used by the helper.
Route decisionsuccessTells the caller whether to continue or reject with a rate-limit response.
Header payloadheadersCarries retry and remaining-budget metadata for API clients.
Trial resource capsTRIAL_LIMITS.api, TRIAL_LIMITS.analyticsApiRepresent plan-state limits for API and analytics API usage during trialing subscriptions.

The Bitly import path demonstrates a second kind of rate-limit handling: respecting an upstream platform's quota. checkIfRateLimited calls Bitly's user/platform_limits endpoint for the "/groups/{group_guid}/bitlinks" path, finds the GET method entry, and compares count with limit. When the current usage is greater than or equal to the limit, Dub does not keep hammering the upstream API. Instead, it calls queueBitlyImport with the original body, marks rateLimited: true, and sets delay: 2 * 60, which schedules another attempt after two minutes.

Sources: apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts

This upstream limiter is intentionally different from the public API limiter. It does not return Dub API response headers, and it does not represent a customer-facing quota. It is operational backpressure around a cron import workflow. That distinction matters when diagnosing failures: a customer receiving 429 Too Many Requests from Dub should inspect Dub's response headers and their plan limits, while a delayed Bitly import may simply be waiting for Bitly's own quota window to recover. The code logs the endpoint data and original body, which helps operators understand why an import was requeued.

Sources: apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts

Extending Rate-Limited Routes

When adding rate limiting to a new Dub API route, start by deciding the bucket identity. For authenticated REST endpoints, that will usually be derived from the authenticated API key, workspace, or user context. Then choose a budget and interval that match the product contract for the endpoint family. Call rateLimitRequest before performing expensive work or mutating state, attach the returned headers to the response, and short-circuit with 429 Too Many Requests when success is false. This sequence gives clients useful feedback while protecting the application and downstream systems.

Sources: apps/web/lib/auth/rate-limit-request.ts

For endpoints that also depend on billing state, do not confuse route throughput with resource entitlement. A workspace might be allowed to make another HTTP request but still be over a trial resource cap such as links, clicks, general API usage, or analytics API usage. The trial-limit helpers provide the effective limit set for a trialing subscription, and the feature phrase helper maps exceeded resources to upgrade-oriented copy such as creating more links, tracking more events, or sending more payouts. Use these helpers for product-limit decisions, not as a replacement for the low-level request limiter.

Sources: packages/utils/src/constants/pricing/trial-limits.ts

For integrations, prefer explicit recovery behavior over blind retries. The Bitly import code checks the provider's own limit endpoint before continuing and uses the queue to delay the next attempt. If you add another provider import, follow the same pattern: read the provider's quota signal, compare usage with the provider-defined limit, annotate the queued job so the retry reason is visible, and choose a delay that respects the provider's reset behavior. This keeps provider throttling isolated from Dub's public API contract while preserving a consistent operational model.

Sources: apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts

Testing and Operational Signals

The most important test signal for rateLimitRequest is that it preserves the public header names and returns string values. Route-level tests should assert both branches: requests within the budget continue and include remaining-budget headers, while requests over the budget return a 429 response with retry metadata. Because the helper receives requests and interval as arguments, endpoint tests can use small windows or mocked limiter behavior to verify the route decision without waiting for real production windows.

Sources: apps/web/lib/auth/rate-limit-request.ts

For plan-limit behavior, test the transition between normal subscription status and trialing. getWorkspaceLimitsForStripeSubscriptionStatus should return the original plan limits when the status is not trialing and should override the trial-constrained resources when it is trialing. That is especially relevant for api and analyticsApi, because those fields can affect how developers experience Dub's API during onboarding. For provider imports, test that a Bitly count equal to limit requeues with rateLimited: true and a two-minute delay, while usage below the limit returns false and allows the import to proceed.

Sources: apps/web/app/(ee)/api/cron/import/bitly/rate-limit.ts, packages/utils/src/constants/pricing/trial-limits.ts

Next Steps

If you are consuming the Dub API, build clients that read the rate-limit headers on every response and back off when Retry-After is present. If you are extending Dub, reuse rateLimitRequest for HTTP request throughput, use the pricing limit helpers for workspace entitlement, and keep upstream provider throttling close to the integration workflow that owns the retry. Related areas to read next are API Authentication for choosing the correct key type, Analytics API for analytics-specific limits, and OpenAPI Specs for how public API behavior is documented for client developers.

Sources: apps/web/lib/auth/rate-limit-request.ts, packages/utils/src/constants/pricing/trial-limits.ts