---
title: "Caching & Rate Limiting"
description: "Dub uses Upstash Redis and Vector infrastructure to power its caching, rate limiting, and event streaming architecture across distributed serverless and edge environments. By leveraging sliding-win..."
last_updated: "2026-10-12T07:00:10.095215+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/caching-rate-limiting"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/upstash/redis.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis.ts)
- [apps/web/lib/upstash/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/index.ts)
- [apps/web/lib/upstash/ratelimit-policies.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts)
- [apps/web/lib/upstash/ratelimit.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit.ts)
- [apps/web/lib/api/links/cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts)
- [apps/web/lib/api/rewards/reward-version.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/rewards/reward-version.ts)
- [apps/web/lib/api/workspaces/workspace-product-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/workspace-product-cache.ts)
- [apps/web/lib/auth/rate-limit-request.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/rate-limit-request.ts)
- [apps/web/lib/upstash/redis-lock.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-lock.ts)
- [apps/web/app/ee/api/cron/sync-redis-resources/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts)
- [apps/web/lib/api/workspaces/onboarding-step-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/onboarding-step-cache.ts)
- [apps/web/lib/auth/token-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/token-cache.ts)
- [apps/web/app/ee/api/cron/streams/update-click-stats/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts)
- [apps/web/lib/api/links/record-click-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/record-click-cache.ts)
- [apps/web/lib/upstash/assert-rate-limit.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/assert-rate-limit.ts)
- [apps/web/app/ee/api/cron/fx-rates/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/fx-rates/route.ts)
- [apps/web/lib/cron/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/cron/index.ts)
- [apps/web/lib/upstash/redis-streams/workspace-links-usage.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/workspace-links-usage.ts)
- [apps/web/lib/jobs/send-jobs.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/jobs/send-jobs.ts)
- [apps/web/lib/analytics/allowed-hostnames-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/allowed-hostnames-cache.ts)
- [apps/web/lib/upstash/redis-streams/client.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/client.ts)
- [apps/web/lib/integrations/shopify/checkout-cache.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/checkout-cache.ts)
- [apps/web/lib/middleware/utils/cache-deeplink-click-data.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/cache-deeplink-click-data.ts)
- [apps/web/lib/upstash/vector.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/vector.ts)
- [apps/web/lib/upstash/redis-streams/workspace-click-events.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/workspace-click-events.ts)
- [apps/web/app/api/unsplash/search/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/unsplash/search/route.ts)
- [apps/web/lib/upstash/redis-streams/link-click-events.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/link-click-events.ts)
- [apps/web/lib/embed/referrals/token-class.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/token-class.ts)
- [apps/web/lib/firstpromoter/importer.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/firstpromoter/importer.ts)
- [apps/web/lib/integrations/google-ads/installed-workspaces.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/google-ads/installed-workspaces.ts)
</details>

## Overview

Dub uses Upstash Redis and Vector infrastructure to power its caching, rate limiting, and event streaming architecture across distributed serverless and edge environments. By leveraging sliding-window rate limiting policies, multi-tiered link resolution caches, and append-only event streams, the system minimizes database load during traffic spikes while maintaining strict consistency across workspaces and domains.

Sources: [apps/web/lib/upstash/redis.ts:1-7](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis.ts#L1-L7), [apps/web/lib/upstash/ratelimit.ts:5-20](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit.ts#L5-L20), [apps/web/lib/api/links/cache.ts:18-21](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L18-L21), [apps/web/lib/upstash/redis-streams/client.ts:8-13](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/client.ts#L8-L13)

## Upstash Client Configuration

### Redis Client Initialization

The application initializes multiple Upstash Redis clients using REST connection parameters drawn from environment variables. The primary `redis` instance connects via `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. A separate global Redis configuration checks for the presence of `UPSTASH_GLOBAL_REDIS_REST_URL` and `UPSTASH_GLOBAL_REDIS_REST_TOKEN`. If both global variables are defined, `redisGlobal` targets the global endpoints; otherwise, it falls back to the standard primary environment variables. A third client, `redisGlobalWithTimeout`, extends the global configuration by supplying an explicit timeout signal via `AbortSignal.timeout(1500)`.

```typescript
import { Redis } from "@upstash/redis";

// Initiate Redis instance by connecting to REST URL
export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL || "",
  token: process.env.UPSTASH_REDIS_REST_TOKEN || "",
});

// This is a separate global Redis instance that we use
// for global operations (e.g. linkCache, recordClick)
// so that if this redis goes down, it won't impact other endpoints
const hasGlobalRedisConfig =
  !!process.env.UPSTASH_GLOBAL_REDIS_REST_URL &&
  !!process.env.UPSTASH_GLOBAL_REDIS_REST_TOKEN;

const redisConfig = {
  url: hasGlobalRedisConfig
    ? process.env.UPSTASH_GLOBAL_REDIS_REST_URL
    : process.env.UPSTASH_REDIS_REST_URL || "",
  token: hasGlobalRedisConfig
    ? process.env.UPSTASH_GLOBAL_REDIS_REST_TOKEN
    : process.env.UPSTASH_REDIS_REST_TOKEN || "",
};

export const redisGlobal = new Redis(redisConfig);

export const redisGlobalWithTimeout = new Redis({
  ...redisConfig,
  signal: () => AbortSignal.timeout(1500),
});
```
Sources: [apps/web/lib/upstash/redis.ts:1-30](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis.ts#L1-L30)

> [!NOTE]
> The `redisGlobalWithTimeout` instance enforces a strict 1500ms timeout on operations using `AbortSignal.timeout(1500)`, preventing stalled global operations from blocking execution endpoints.

### Vector Index Initialization

Vector operations rely on two distinct Upstash Vector `Index` instances initialized with dedicated REST credentials. The standard `vectorIndex` connects using `UPSTASH_VECTOR_REST_URL` and `UPSTASH_VECTOR_REST_TOKEN`. The `emojiVectorIndex` targets emoji-specific vector embeddings by utilizing `UPSTASH_VECTOR_EMOJI_REST_URL` and `UPSTASH_VECTOR_EMOJI_REST_TOKEN`, typing its metadata entries with strict `emoji` and `label` string properties.

```typescript
import { Index } from "@upstash/vector";

export const vectorIndex = new Index({
  url: process.env.UPSTASH_VECTOR_REST_URL ?? "",
  token: process.env.UPSTASH_VECTOR_REST_TOKEN ?? "",
});

export const emojiVectorIndex = new Index<{
  emoji: string;
  label: string;
}>({
  url: process.env.UPSTASH_VECTOR_EMOJI_REST_URL ?? "",
  token: process.env.UPSTASH_VECTOR_EMOJI_REST_TOKEN ?? "",
});
```
Sources: [apps/web/lib/upstash/vector.ts:1-14](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/vector.ts#L1-L14)

## Link Resolution Cache Architecture

### Key Derivation and Multi-Tiered Lookup

The link resolution cache relies on a multi-tiered architecture combining an in-memory `LRUCache`, a distributed `redisGlobal` data store, and a Vercel runtime cache fallback. Cache keys for link lookups are generated via `_createKey`, which evaluates domain case sensitivity. If a domain is case-sensitive, the key is decoded using `decodeKey`; otherwise, keys are forced to lowercase with a `linkcache:{domain}:{originalKey}` prefix.

```typescript
const linkLRUCache = new LRUCache<string, RedisLinkProps>({
  max: 10000, // max 10,000 entries
  ttl: 5000, // 5 seconds
});
```
Sources: [apps/web/lib/api/links/cache.ts:18-21](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L18-L21)

```typescript
async get({ domain, key }: Pick<LinkProps, "domain" | "key">): Promise<{
  cachedLink: RedisLinkProps | null;
  redisFailOver: boolean;
}> {
  // here we use linkcache:${domain}:${key} instead of this._createKey({ domain, key })
  // because the key can either be cached as case-sensitive or case-insensitive depending on the domain
  // so we should get the original key from the cache
  const cacheKey = `linkcache:${domain}:${key}`;
```
Sources: [apps/web/lib/api/links/cache.ts:71-79](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L71-L79)

```typescript
_createKey({ domain, key }: Pick<LinkProps, "domain" | "key">) {
  const caseSensitive = isCaseSensitiveDomain(domain);
  const originalKey = caseSensitive ? decodeKey(key) : key;
  const cacheKey = `linkcache:${domain}:${originalKey}`;

  return caseSensitive ? cacheKey : cacheKey.toLowerCase();
}
```
Sources: [apps/web/lib/api/links/cache.ts:173-179](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L173-L179)

> [!WARNING]
> When retrieving links, lookups use `linkcache:${domain}:${key}` instead of `_createKey` directly. Because keys can be cached as case-sensitive or case-insensitive depending on the domain configuration, this format retrieves the exact original key stored in the cache.

### Serialization and Expiration Constants

Link objects are formatted into serialized Redis payloads before storage. Cache expirations differ across storage layers to balance durability and freshness: the local LRU cache holds up to 10,000 entries with a 5-second TTL, the Vercel runtime cache expires after 5 minutes (300 seconds), and the global Redis cache persists records for 24 hours.

| Cache Tier | Storage Mechanism | Expiration / Limit | Sources |
| :--- | :--- | :--- | :--- |
| **LRU Cache** | `linkLRUCache` (`lru-cache`) | Max 10,000 entries, 5-second TTL (`5000`) | [apps/web/lib/api/links/cache.ts:18-21](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L18-L21) |
| **Vercel Runtime Cache** | `vercelCache` (`@vercel/functions`) | 5 minutes (`60 * 5`) | [apps/web/lib/api/links/cache.ts:27-29](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L27-L29) |
| **Global Redis Cache** | `redisGlobal` (`@upstash/redis`) | 24 hours (`60 * 60 * 24`) | [apps/web/lib/api/links/cache.ts:30](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L30-L30) |

Sources: [apps/web/lib/api/links/cache.ts:18-30](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L18-L30)

### Invalidation and Failover Mechanisms

Cache invalidation occurs through `revalidateTag` combined with static page tags formatted as `static:{domain}:{key}` and `static:{domain}`. When invalidating entries in the Vercel runtime cache, the `_invalidateVercelRuntimeCache` method checks if a value is cached before deleting it to optimize operations, as runtime cache reads are significantly cheaper than writes. 

```typescript
_createStaticPagesCacheKeys({
  domain,
  key,
}: Pick<LinkProps, "domain" | "key">) {
  domain = domain.toLowerCase();
  // here we set 2 cache tags to invalidate the cache:
  // 1. static:${domain}:${key} - for the specific static page
  // 2. static:${domain} - scope all links under the domain (for easy purging in PATCH /domains/:domain)
  return `static:${domain}:${key},static:${domain}`;
}
```
Sources: [apps/web/lib/api/links/cache.ts:181-190](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L181-L190)

```typescript
private async _invalidateVercelRuntimeCache(cacheKey: string) {
  return vercelCache
    .get(cacheKey)
    .then((cachedLink) =>
      cachedLink ? vercelCache.delete(cacheKey) : undefined,
    );
}
```
Sources: [apps/web/lib/api/links/cache.ts:194-200](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L194-L200)

> [!NOTE]
> If `redisGlobalWithTimeout` throws an error during lookups, the system triggers a failover sequence: it queries the Vercel runtime cache, falls back to `getLinkViaEdge` querying MySQL if missing, and asynchronously repopulates both the LRU and Vercel caches using `waitUntil`.

Sources: [apps/web/lib/api/links/cache.ts:105-135](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/cache.ts#L105-L135)

## Rate Limiting Engine Policies

### Algorithm Setup and Policy Tiers

The rate limiting engine uses `@upstash/ratelimit` configured with a sliding window algorithm and a default capacity of 10 requests per 10 seconds. The base `ratelimit` helper initializes the `@upstash/ratelimit` instance using the shared Redis client, enabling analytics, a request timeout of 1000ms, and a `dub` key prefix.

```typescript
export const ratelimit = (
  requests: number = 10,
  seconds:
    | `${number} ms`
    | `${number} s`
    | `${number} m`
    | `${number} h`
    | `${number} d` = "10 s",
) => {
  return new Ratelimit({
    redis: redis,
    limiter: Ratelimit.slidingWindow(requests, seconds),
    analytics: true,
    prefix: "dub",
    timeout: 1000,
  });
```
Sources: [apps/web/lib/upstash/ratelimit.ts:5-20](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit.ts#L5-L20)

Predefined rate limit policies define exact attempt counts, time windows, and key prefixes for various sensitive operations. Policies are enforced via `RATELIMIT_POLICIES`, which maps identifiers like login attempts, file uploads, and search routines to strict quotas.

| Policy Name | Attempts | Window | Key Prefix | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `login` | 5 | 1 m | `rl:auth:login` | [apps/web/lib/upstash/ratelimit-policies.ts:19-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L19-L24) |
| `loginLinkSend` | 2 | 1 m | `rl:auth:login-link:send` | [apps/web/lib/upstash/ratelimit-policies.ts:26-30](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L26-L30) |
| `signupOtpSend` | 2 | 1 m | `rl:auth:signup:otp:send` | [apps/web/lib/upstash/ratelimit-policies.ts:32-36](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L32-L36) |
| `accountExistsCheck` | 8 | 1 m | `rl:auth:account-exists:check` | [apps/web/lib/upstash/ratelimit-policies.ts:39-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L39-L42) |
| `passwordResetRequest` | 2 | 1 m | `rl:auth:password-reset:request` | [apps/web/lib/upstash/ratelimit-policies.ts:44-48](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L44-L48) |
| `passwordReset` | 2 | 1 m | `rl:auth:password-reset:confirm` | [apps/web/lib/upstash/ratelimit-policies.ts:50-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L50-L54) |
| `emailChangeRequest` | 3 | 24 h | `rl:auth:email-change` | [apps/web/lib/upstash/ratelimit-policies.ts:56-60](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L56-L60) |
| `emailChangeRequestTarget` | 3 | 24 h | `rl:auth:email-change:target` | [apps/web/lib/upstash/ratelimit-policies.ts:63-67](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L63-L67) |
| `samlVerify` | 10 | 1 m | `rl:auth:saml-verify` | [apps/web/lib/upstash/ratelimit-policies.ts:69-73](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L69-L73) |
| `domainSearchAvailability` | 1 | 5 s | `rl:domains:search-availability` | [apps/web/lib/upstash/ratelimit-policies.ts:339-344](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L339-L344) |
| `emojiSearch` | 30 | 10 s | `rl:ai:emoji-search` | [apps/web/lib/upstash/ratelimit-policies.ts:346-351](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L346-L351) |

Sources: [apps/web/lib/upstash/ratelimit-policies.ts:18-352](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L18-L352)

### Request Enforcement via assertRateLimit

The `assertRateLimit` function evaluates incoming requests against a given `RatelimitPolicy`. It skips enforcement if `shouldApplyRateLimit` evaluates to false, constructs the Redis key by joining the `policy.keyPrefix` and dynamic identifier segments, and queries the sliding window limiter.

```typescript
export async function assertRateLimit({
  policy,
  identifier,
}: {
  policy: RatelimitPolicy;
  identifier: string | string[]; // dynamic part(s) of the key, e.g. email or [email, ip]
}) {
  if (!shouldApplyRateLimit) {
    return;
  }

  const key = [
    policy.keyPrefix,
    ...(Array.isArray(identifier) ? identifier : [identifier]),
  ].join(":");

  const { success, reset } = await ratelimit(
    policy.attempts,
    policy.window,
  ).limit(key);
```
Sources: [apps/web/lib/upstash/assert-rate-limit.ts:28-47](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/assert-rate-limit.ts#L28-L47)

If the request exceeds permitted attempts, `assertRateLimit` computes a human-friendly duration via `formatRetryAfter` and throws a `DubApiError` with the `rate_limit_exceeded` code and policy-specific error message.

```typescript
if (!success) {
  const retryAfter = formatRetryAfter(reset);

  const message =
    typeof policy.message === "function"
      ? policy.message({
          retryAfter,
          attempts: policy.attempts,
          window: policy.window,
        })
      : policy.message ??
        `Too many requests. Please try again in ${retryAfter}.`;

  throw new DubApiError({
    code: "rate_limit_exceeded",
    message,
  });
}
```
Sources: [apps/web/lib/upstash/assert-rate-limit.ts:49-66](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/assert-rate-limit.ts#L49-L66)

> [!WARNING]
> The `login` policy defines a custom error message string (`"too-many-login-attempts"`) that is matched verbatim by the sign-in page. Modifying this string breaks error handling in the authentication UI.

Sources: [apps/web/lib/upstash/ratelimit-policies.ts:19-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/ratelimit-policies.ts#L19-L24)

Alternative enforcement occurs in edge routes like Unsplash search, which directly invokes `ratelimit(10, "10 s").limit(...)` using the caller's IP address from `@vercel/functions` and responds with HTTP status 429 upon failure.

```typescript
const ip = ipAddress(req);
const { success } = await ratelimit(10, "10 s").limit(`unsplash:${ip}`);
if (!success) {
  return new Response("Don't DDoS me pls 🥺", { status: 429 });
}
```
Sources: [apps/web/app/api/unsplash/search/route.ts:20-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/unsplash/search/route.ts#L20-L24)

## Distributed Concurrency Locks

```typescript
export async function withRedisLock<T>({
  key,
  ttlSeconds,
  fn,
}: {
  key: string;
  ttlSeconds: number;
  fn: () => Promise<T>;
}): Promise<T | null> {
  const token = crypto.randomUUID();
  const acquired = await redis.set(key, token, {
    nx: true,
    ex: ttlSeconds,
  });

  if (!acquired) {
    return null;
  }

  try {
    return await fn();
  } finally {
    // Lua compare-and-delete: only the token owner may release the lock.
    await redis.eval(RELEASE_LOCK_SCRIPT, [key], [token]);
  }
}
```
Sources: [apps/web/lib/upstash/redis-lock.ts:17-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-lock.ts#L17-L42)

The `withRedisLock` function implements mutual exclusion by executing critical-section routines under an Upstash Redis NX lock. It generates a unique identifier using `crypto.randomUUID()` and attempts to set the lock key with `{ nx: true, ex: ttlSeconds }`. If acquisition fails because another holder is active, the function immediately returns `null` without running the wrapped callback.

```typescript
const RELEASE_LOCK_SCRIPT = `
  if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
  else
    return 0
  end
`;
```
Sources: [apps/web/lib/upstash/redis-lock.ts:3-9](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-lock.ts#L3-L9)

To prevent race conditions where an expired lock acquired by another runner is mistakenly deleted, the `finally` block evaluates a Lua compare-and-delete script. This script verifies that the stored value matches the original execution token before executing `redis.call("del", KEYS[1])`.

> [!CAUTION]
> Releasing a Redis lock without verifying the unique token risks deleting a lock belonging to a different process whose execution exceeded the original `ttlSeconds` threshold.

Sources: [apps/web/lib/upstash/redis-lock.ts:14-15](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-lock.ts#L14-L15)

## Application Domain Entity Caching

### Workspace, Onboarding, and Embed Token Caches

Dub implements specialized ephemeral caching layers for domain entities using Upstash Redis. These caches reduce database load for frequently accessed workspace metadata, onboarding progression, authentication tokens, tracking hostnames, and third-party checkout flows.

Workspace products are cached using keys prefixed with `workspace:product` for a duration of 30 days. The `WorkspaceProductCache` class handles retrieval, persistence, and deletion of `DubProduct` records indexed by workspace slug.

```typescript
const CACHE_KEY_PREFIX = "workspace:product";
const CACHE_EXPIRATION = 60 * 60 * 24 * 30; // 30 days

class WorkspaceProductCache {
  async get({ slug }: { slug: string }) {
    return await redis.get<DubProduct>(this._createKey({ slug }));
  }

  async set({ slug, product }: { slug: string; product: DubProduct }) {
    return await redis.set(this._createKey({ slug }), product, {
      ex: CACHE_EXPIRATION,
    });
  }

  async delete({ slug }: { slug: string }) {
    return await redis.del(this._createKey({ slug }));
  }

  _createKey({ slug }: { slug: string }) {
    return `${CACHE_KEY_PREFIX}:${slug}`;
  }
}
```
Sources: [apps/web/lib/api/workspaces/workspace-product-cache.ts:4-25](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/workspace-product-cache.ts#L4-L25)

Onboarding progression states are tracked via `OnboardingStepCache` under the `onboarding-step` prefix with a 24-hour expiration window. It supports single-user operations and batched pipeline updates across multiple user identifiers.

```typescript
const CACHE_KEY_PREFIX = "onboarding-step";
export const ONBOARDING_WINDOW_SECONDS = 60 * 60 * 24; // 24 hours

class OnboardingStepCache {
  async set({ userId, step }: { userId: string; step: OnboardingStep }) {
    return await redis.set(`${CACHE_KEY_PREFIX}:${userId}`, step, {
      ex: ONBOARDING_WINDOW_SECONDS,
    });
  }

  async mset({ userIds, step }: { userIds: string[]; step: OnboardingStep }) {
    const pipeline = redis.pipeline();
    userIds.forEach((userId) => {
      pipeline.set(`${CACHE_KEY_PREFIX}:${userId}`, step, {
        ex: ONBOARDING_WINDOW_SECONDS,
      });
    });
    return await pipeline.exec();
  }

  async get({ userId }: { userId: string }) {
    return await redis.get(`${CACHE_KEY_PREFIX}:${userId}`);
  }
}
```
Sources: [apps/web/lib/api/workspaces/onboarding-step-cache.ts:4-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/onboarding-step-cache.ts#L4-L27)

Embed tokens for referrals are generated with a unique identifier prefix and stored with an NX condition to prevent collision.

```typescript
class ReferralsEmbedToken {
  async create(props: ReferralsEmbedTokenProps) {
    const publicToken = createId({
      prefix: EMBED_PUBLIC_TOKEN_PREFIX,
    });

    await redis.set(publicToken, JSON.stringify(props), {
      ex: EMBED_PUBLIC_TOKEN_EXPIRY,
      nx: true,
    });

    return {
      publicToken,
      expires: new Date(Date.now() + EMBED_PUBLIC_TOKEN_EXPIRY * 1000),
    };
  }

  async get(token: string) {
    return await redis.get<ReferralsEmbedTokenProps>(token);
  }
}
```
Sources: [apps/web/lib/embed/referrals/token-class.ts:13-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/embed/referrals/token-class.ts#L13-L33)

| Cache Manager | Key Prefix | TTL | Primary Data Model |
| :--- | :--- | :--- | :--- |
| `workspaceProductCache` | `workspace:product` | 30 days | `DubProduct` |
| `onboardingStepCache` | `onboarding-step` | 24 hours | `OnboardingStep` |
| `tokenCache` | `dubTokenCache` | 24 hours | `TokenCacheItem` |
| `allowedHostnamesCache` | `allowedHostnamesCache` | 7 days | `string` (joined hostnames) |
| `shopifyCheckoutCache` | `shopify:checkout:` | 1 hour | `ShopifyCheckoutCacheItem` |

Sources: [apps/web/lib/api/workspaces/workspace-product-cache.ts:4-5](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/workspace-product-cache.ts#L4-L5), [apps/web/lib/api/workspaces/onboarding-step-cache.ts:4-5](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/workspaces/onboarding-step-cache.ts#L4-L5), [apps/web/lib/auth/token-cache.ts:4-5](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/token-cache.ts#L4-L5), [apps/web/lib/analytics/allowed-hostnames-cache.ts:3-4](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/allowed-hostnames-cache.ts#L3-L4), [apps/web/lib/integrations/shopify/checkout-cache.ts:6-7](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/checkout-cache.ts#L6-L7)

### Authentication Token and Hostname Caches

Restricted API tokens and legacy personal access tokens are cached via `TokenCache` using the `dubTokenCache` prefix and a 24-hour expiration. Retrieved items are validated against the `tokenCacheItemSchema` Zod definition before use, ensuring type safety for embedded user objects, project plans, trial dates, and scope strings.

```typescript
const tokenCacheItemSchema = z.object({
  id: z.string().nullish(),
  expires: z.date().nullish(),
  user: z.object({
    id: z.string(),
    name: z.string().nullable(),
    email: z.string().nullable(),
    isMachine: z.boolean(),
    defaultWorkspace: z.string().nullish(),
    defaultPartnerId: z.string().nullish(),
  }),
  scopes: z.string().nullish(),
  projectId: z.string().nullish(),
  project: z
    .object({
      plan: z.string().nullish(),
      trialEndsAt: z.coerce.date().nullish(),
    })
    .nullish(),
  installationId: z.string().nullish(),
});
```
Sources: [apps/web/lib/auth/token-cache.ts:7-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/token-cache.ts#L7-L27)

When tokens are revoked or modified, `expireMany` uses an Upstash pipeline to set the TTL of multiple hashed keys to 1 second.

```typescript
async expireMany({ hashedKeys }: { hashedKeys: string[] }) {
  if (hashedKeys.length === 0) {
    return;
  }

  const pipeline = redis.pipeline();

  hashedKeys.forEach((hashedKey) => {
    pipeline.expire(this._createKey({ hashedKey }), 1);
  });

  return await pipeline.exec();
}
```
Sources: [apps/web/lib/auth/token-cache.ts:57-69](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/auth/token-cache.ts#L57-L69)

Allowed hostnames for analytics tracking are managed by `AllowedHostnamesCache` with a 7-day TTL, using batched pipelines for multi-domain sets.

```typescript
class AllowedHostnamesCache {
  async mset({
    allowedHostnames,
    domains,
  }: {
    allowedHostnames: string;
    domains: string[];
  }) {
    if (domains.length === 0) {
      return;
    }

    const pipeline = redis.pipeline();

    domains.forEach((domain) => {
      pipeline.set(this._createKey({ domain }), allowedHostnames, {
        ex: CACHE_EXPIRATION,
      });
    });

    return await pipeline.exec();
  }
```
Sources: [apps/web/lib/analytics/allowed-hostnames-cache.ts:6-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/analytics/allowed-hostnames-cache.ts#L6-L27)

### Shopify Checkout States and Concurrency Dispatches

Shopify checkouts are cached as Redis hashes under `shopify:checkout:` with a 1-hour TTL. The `ShopifyCheckoutCache` parses hash structures through `shopifyCheckoutCacheSchema`, normalizing nullable click identifiers.

```typescript
const shopifyCheckoutCacheSchema = z.object({
  clickId: z
    .string()
    .nullish()
    .transform((value) => value ?? undefined),
  workspaceId: z.string().optional(),
  order: shopifyOrderSchema.optional(),
  dispatched: z.boolean().optional(),
});

type ShopifyCheckoutCacheItem = z.infer<typeof shopifyCheckoutCacheSchema>;
```
Sources: [apps/web/lib/integrations/shopify/checkout-cache.ts:9-19](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/checkout-cache.ts#L9-L19)

The `tryDispatchShopifyOrderJob` function manages atomic dispatch claims to prevent duplicate processing of the same checkout token. It verifies that the checkout contains an order, workspace ID, and click ID, checks that it has not already been dispatched, and attempts an atomic claim via `hsetnx`.

```typescript
if (!checkout.order || !checkout.workspaceId || !checkout.clickId) {
  console.info(
    "Shopify order dispatch skipped: checkout incomplete",
    logContext,
  );
  return false;
}

if (checkout.dispatched) {
  console.info(
    "Shopify order dispatch skipped: already dispatched",
    logContext,
  );
  return false;
}

// Claim the checkout
const key = shopifyCheckoutCache.createKey(checkoutToken);
const claim = await redis.hsetnx(key, "dispatched", true);
const claimed = Boolean(claim);

if (!claimed) {
  console.info("Shopify order dispatch skipped: claim lost", logContext);
  return false;
}
```
Sources: [apps/web/lib/integrations/shopify/checkout-cache.ts:77-101](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/checkout-cache.ts#L77-L101)

> [!WARNING]
> If job dispatch fails after acquiring the checkout claim, the catch block explicitly deletes the `dispatched` hash field to release the lock, ensuring that transient queue failures do not permanently strand orders.

Sources: [apps/web/lib/integrations/shopify/checkout-cache.ts:114-121](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/shopify/checkout-cache.ts#L114-L121)

## Stream Ingestion and Consumption

### Event Producers and Fallback Pipelines

Dub utilizes append-only Redis streams via the `RedisStream` abstraction class to buffer high-frequency write events such as link clicks, workspace click webhook events, and workspace links usage updates. This architecture decouples synchronous request handling from heavy database updates by persisting events into streams via `redis.xadd` and processing them asynchronously in cron routines or batched consumers.

```typescript
export class RedisStream {
  private streamKey: string;

  constructor(streamKey: string) {
    this.streamKey = streamKey;
  }

  /**
   * Read and process a batch of items from the stream
   * This provides a consumable batch of messages with the ability to delete them upon successful completion of the handler
   */
  async processBatch<T>(
    handler: (
      records: RedisStreamEntry<T>[],
    ) => Promise<any & { processedEntryIds: string[] }>,
    options: {
      startId?: string;
      endId?: string;
      count?: number;
      deleteAfterRead?: boolean;
    } = {},
  ): Promise<any> {
    const {
      startId = "0",
      endId = "+",
      count = 1000,
      deleteAfterRead = true,
    } = options;

    try {
      // Read entries from the stream
      const entriesMap = await redis.xrange(
        this.streamKey,
        startId,
        endId,
        count,
      );

      const entries: RedisStreamEntry<T>[] = Object.entries(entriesMap).map(
        ([id, data]) => ({
          id,
          data: data as any,
        }),
      );

      const { processedEntryIds, ...response } = await handler(entries);

      // Optionally delete processed entries to prevent memory buildup
      if (deleteAfterRead) {
        try {
          if (processedEntryIds.length > 0) {
            // xdel supports deleting multiple IDs at once
            await redis.xdel(this.streamKey, processedEntryIds);
          }
        } catch (error) {
          console.warn("Failed to clean up processed stream entries:", error);
          // Don't throw - this is not critical
        }
      }

      return response as Response;
    } catch (error) {
      console.error(
        "Failed to read workspace usage updates from stream:",
        error,
      );
      throw error;
    }
  }
```
Sources: [apps/web/lib/upstash/redis-streams/client.ts:8-76](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/client.ts#L8-L76)

Event streams are managed by dedicated modules that validate payloads before publishing. The `publishLinkClickEvent` function pushes link click payloads to `link:click:events`, capturing metrics for link click counts, workspace usage, and program enrollments. If Redis ingestion fails, it falls back to direct database execution using `conn.execute` for PlanetScale alongside error reporting via Axiom.

```typescript
// Publishes a link click event to the stream to update:
// - link clicks count + lastClicked timestamp
// - workspace usage + totalClicks
// - program enrollment totalClicks
export const publishLinkClickEvent = async ({
  linkId,
  timestamp,
  workspaceId,
  programId,
  partnerId,
}: LinkClickEvent) => {
  const payload = {
    linkId,
    timestamp,
    ...(workspaceId && { workspaceId }),
    ...(programId && partnerId && { programId, partnerId }),
  };

  try {
    return await redis.xadd(STREAM_KEY, "*", payload);
  } catch (error) {
    logger.error("stream.publish_failed", {
      service: "upstash",
      streamKey: STREAM_KEY,
      error: toErrorFields(error),
      correlation: {
        linkId,
        workspaceId,
        programId,
        partnerId,
      },
    });

    return await Promise.allSettled([
      conn.execute(
        "UPDATE Link SET clicks = clicks + 1, lastClicked = NOW() WHERE id = ?",
        [linkId],
      ),

      workspaceId
        ? conn.execute(
            "UPDATE Project p SET p.usage = p.usage + 1, p.totalClicks = p.totalClicks + 1 WHERE id = ?",
            [workspaceId],
          )
        : null,

      programId && partnerId
        ? conn.execute(
            "UPDATE ProgramEnrollment SET totalClicks = totalClicks + 1 WHERE programId = ? AND partnerId = ?",
            [programId, partnerId],
          )
        : null,

      logger.flush(),
    ]);
  }
```
Sources: [apps/web/lib/upstash/redis-streams/link-click-events.ts:18-73](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/link-click-events.ts#L18-L73)

Similarly, `publishWorkspaceLinksUsageEvent` writes workspace link changes to `workspace:linksUsage:updates`. When network issues prevent stream publication, it executes a fallback SQL statement updating `linksUsage` and `totalLinks` directly on the `Project` table.

```typescript
export const publishWorkspaceLinksUsageEvent = async (
  event: WorkspaceLinksUsageEvent,
) => {
  const { workspaceId, linksCount, timestamp } = event;

  try {
    return await redis.xadd(WORKSPACE_LINKS_USAGE_UPDATES_STREAM_KEY, "*", {
      workspaceId,
      linksCount,
      timestamp,
    });
  } catch (error) {
    console.error(
      "Failed to publish workspace links usage event (falling back to database update):",
      error,
    );
    // fallback on writing directly to the database
    return await conn.execute(
      "UPDATE Project SET linksUsage = linksUsage + ?, totalLinks = totalLinks + ? WHERE id = ?",
      [linksCount, linksCount, workspaceId],
    );
  }
```
Sources: [apps/web/lib/upstash/redis-streams/workspace-links-usage.ts:19-40](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/workspace-links-usage.ts#L19-L40)

Workspace-specific click events for workspaces subscribed to webhooks are parsed against `clickEventSchemaTB`, verified against `clickWebhookWorkspaces`, and appended to `workspace:click:events`.

```typescript
export const publishWorkspaceClickEvent = async (event) => {
  try {
    const parsedEvent = clickEventSchemaTB.parse({
      ...event,
      qr: Number(event.qr),
      bot: Number(event.bot),
    });

    const hasLinkClickedWebhooks = await clickWebhookWorkspaces.has(
      parsedEvent.workspace_id,
    );

    if (!hasLinkClickedWebhooks) {
      return;
    }

    return await redis.xadd(STREAM_KEY, "*", parsedEvent);
  } catch (error) {
    logger.error("stream.publish_failed", {
      service: "upstash",
      streamKey: STREAM_KEY,
      error: toErrorFields(error),
      correlation: {
        workspaceId: event.workspace_id,
        clickId: event.click_id,
        linkId: event.link_id,
      },
    });

    await logger.flush();
    throw error;
  }
```
Sources: [apps/web/lib/upstash/redis-streams/workspace-click-events.ts:12-43](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/upstash/redis-streams/workspace-click-events.ts#L12-L43)

### Stream Parameters and Aggregation Configuration

The click statistics cron worker defines control constants and thresholds governing batch consumption, backpressure alerts, and concurrency limits.

| Constant Name | Value | Purpose |
| :--- | :--- | :--- |
| `BATCH_SIZE` | `10_000` | Max stream entries to consume per cron run |
| `SUB_BATCH_SIZE` | `50` | DB updates to run in parallel within each batch |
| `BACKLOG_ALERT_THRESHOLD` | `50_000` | Alert when stream length exceeds this count |
| `BACKLOG_AGE_ALERT_MS` | `300_000` (5 mins) | Alert when oldest pending entry exceeds age limit |
| `LOCK_KEY` | `"lock:update-click-stats"` | Prevents concurrent GET/POST double-counting |
| `LOCK_TTL_SECONDS` | `600` | Lock TTL matching cron maxDuration |

Sources: [apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:15-20](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts#L15-L20)

> [!NOTE]
> The `aggregateClickStats` function processes raw `LinkClickEvent` entries into maps for link updates, workspace usage increments, and program partner enrollment click tallies, resolving missing or invalid timestamps to `Date.now()`.

Sources: [apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:40-112](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts#L40-L112)

## State Synchronization and Reconciliation

### Scheduled Cron Synchronization Routines

Dub uses recurring cron routines to synchronize external states, clean up redundant metadata, and rebuild transient Redis resources from persistent databases. The synchronization endpoint executes every five minutes via `withCron` and coordinates multiple reconciliation routines across workspace integrations and webhook entries.

```typescript
import { withCron } from "@/lib/cron/with-cron";
import { syncGoogleAdsInstalledWorkspaceSet } from "@/lib/integrations/google-ads/installed-workspaces";
import { logAndRespond } from "../utils";
import { cleanupRedundantLinkWebhookEntries } from "./cleanup-redundant-link-webhook-entries";
import { syncClickWebhookWorkspaceSet } from "./sync-click-webhook-workspace-set";

export const dynamic = "force-dynamic";

/*
    GET /api/cron/sync-redis-resources
    Rebuild various Redis resources:
    - syncClickWebhookWorkspaceSet: rebuild the Redis set of workspaces with active link.clicked webhooks
    - cleanupRedundantLinkWebhookEntries: remove redundant LinkWebhook entries for webhooks that are not scoped to links (folders, workspace)
    - syncGoogleAdsInstalledWorkspaceSet: rebuild the Redis set of workspaces with Google Ads installed
*/

// Runs every 5 minutes (*/5 * * * *)
export const GET = withCron(async () => {
  const result = await Promise.allSettled([
    syncClickWebhookWorkspaceSet(),
    cleanupRedundantLinkWebhookEntries(),
    syncGoogleAdsInstalledWorkspaceSet(),
  ]);

  [
    "syncClickWebhookWorkspaceSet",
    "cleanupRedundantLinkWebhookEntries",
    "syncGoogleAdsInstalledWorkspaceSet",
  ].map((name, index) => {
    const settled = result[index];
    if (settled.status === "fulfilled") {
      console.log(`${name}: ${settled.value}`);
    } else if (settled.status === "rejected") {
      console.error(`${name}: ${settled.reason}`);
    } else {
      console.error(`${name}: unknown error`);
    }
  });

  return logAndRespond("Synced Redis resources.");
});
```
Sources: [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:1-41](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L1-L41)

The synchronization pipeline delegates tasks using `Promise.allSettled` to guarantee that individual resource failures do not block other reconciliation routines. The executed routines and their integration behaviors are summarized below.

| Routine Function | Source File | Reconciliation Target & Purpose |
| :--- | :--- | :--- |
| `syncClickWebhookWorkspaceSet` | `apps/web/app/ee/api/cron/sync-redis-resources/sync-click-webhook-workspace-set.ts` | Rebuilds the Redis set of workspaces maintaining active link click webhooks. |
| `cleanupRedundantLinkWebhookEntries` | `apps/web/app/ee/api/cron/sync-redis-resources/cleanup-redundant-link-webhook-entries.ts` | Removes redundant `LinkWebhook` database entries unlinked from specific links (e.g., scoped to folders or workspaces). |
| `syncGoogleAdsInstalledWorkspaceSet` | `apps/web/lib/integrations/google-ads/installed-workspaces.ts` | Rebuilds the Redis set for workspaces with active Google Ads integrations. |

Sources: [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:2-5](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L2-L5), [apps/web/lib/integrations/google-ads/installed-workspaces.ts:25-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/google-ads/installed-workspaces.ts#L25-L54)

> [!NOTE]
> `syncGoogleAdsInstalledWorkspaceSet` queries Prisma for installed integrations filtered by `GOOGLE_ADS_INTEGRATION_ID` and advanced or enterprise workspace plans. It writes updates into a temporary Redis key (`googleAdsInstalledWorkspaces:tmp`) before performing an atomic rename to replace the production `googleAdsInstalledWorkspaces` set, avoiding race conditions during reads.

Sources: [apps/web/lib/integrations/google-ads/installed-workspaces.ts:25-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/google-ads/installed-workspaces.ts#L25-L54)

### Versioned Keys and Cache Invalidation

Cache invalidation for reward events and multi-tenant data dependencies relies on versioned Redis keys managed through incrementing version counters. The reward versioning utilities define keys prefixed by workspace and event type, maintaining a default time-to-live of 24 hours.

```typescript
import { redis } from "@/lib/upstash";
import { EventType } from "@prisma/client";

const REWARD_VERSION_TTL_SECONDS = 24 * 60 * 60; // 24 hours

export function getRewardVersionKey({
  groupId,
  event,
}: {
  groupId: string;
  event: EventType; // Reward type
}) {
  return `reward-version:${groupId}:${event}`;
}

export async function incrementRewardVersion({
  groupId,
  event,
}: {
  groupId: string;
  event: EventType;
}) {
  const key = getRewardVersionKey({
    groupId,
    event,
  });

  const version = await redis.incr(key);
  await redis.expire(key, REWARD_VERSION_TTL_SECONDS);
  return version;
}

export async function isStaleRewardVersion({
  version,
  groupId,
  event,
}: {
  version: number;
  groupId: string;
  event: EventType;
}) {
  const key = getRewardVersionKey({
    groupId,
    event,
  });

  const currentVersion = await redis.get(key);

  return currentVersion != null && version < Number(currentVersion);
}
```
Sources: [apps/web/lib/api/rewards/reward-version.ts:1-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/rewards/reward-version.ts#L1-L50)

The reward versioning module exposes three core functions to handle cache invalidation checks and version bumps:

- `getRewardVersionKey({ groupId, event })`: Constructs the Redis key string formatted as `reward-version:${groupId}:${event}`.
- `incrementRewardVersion({ groupId, event })`: Atomically increments the version counter via `redis.incr` and resets its expiration to 24 hours (`REWARD_VERSION_TTL_SECONDS`).
- `isStaleRewardVersion({ version, groupId, event })`: Compares an incoming version number against the current Redis version, returning `true` if a newer version exists.

Sources: [apps/web/lib/api/rewards/reward-version.ts:4-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/rewards/reward-version.ts#L4-L50)

## Related

- [Request Routing](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/request-routing)
- [Link Dispatch](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-attribution/link-dispatch)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
