---
title: "Caching and Edge Config"
description: "Dub relies on a multi-tiered caching and edge configuration architecture to minimize database load, maintain high availability during traffic spikes, and ensure ultra-low latency link resolution at..."
last_updated: "2026-10-05T05:07:35.174967+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/caching-and-edge-config"
---

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

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

- [apps/web/lib/api/links/cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts)
- [apps/web/lib/upstash/redis.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts)
- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts)
- [apps/web/app/api/domains/domain/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/lib/upstash/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/index.ts)
- [apps/web/lib/planetscale/get-link-via-edge.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts)
- [apps/web/app/ee/api/cron/domains/update/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts)
- [apps/web/lib/api/links/record-click-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts)
- [apps/web/app/api/workspaces/idOrSlug/import/short/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/import/short/route.ts)
- [apps/web/lib/planetscale/get-domain-via-edge.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-domain-via-edge.ts)
- [apps/web/lib/analytics/allowed-hostnames-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/allowed-hostnames-cache.ts)
- [apps/web/app/ee/api/cron/sync-redis-resources/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts)
- [apps/web/lib/fetchers/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/fetchers/index.ts)
- [apps/web/lib/middleware/utils/crawl-bitly.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts)
- [apps/web/app/ee/api/cron/streams/update-click-stats/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts)
- [apps/web/lib/middleware/utils/cache-deeplink-click-data.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/cache-deeplink-click-data.ts)
- [apps/web/lib/api/workspaces/workspace-product-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/workspace-product-cache.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/page-client.tsx)
- [apps/web/lib/planetscale/get-shortlink-via-edge.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-shortlink-via-edge.ts)
- [apps/web/app/ee/api/cron/cleanup/link-retention/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/cleanup/link-retention/route.ts)
- [apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts)
- [apps/web/lib/upstash/ratelimit-policies.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts)
- [apps/web/app/api/domains/client/saved/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/client/saved/route.ts)
- [apps/web/app/ee/api/cron/links/invalidate-for-partners/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/links/invalidate-for-partners/route.ts)
- [apps/web/lib/auth/token-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts)
- [apps/web/app/wellknown/domain/file/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/wellknown/%5Bdomain%5D/%5Bfile%5D/route.ts)
- [apps/web/lib/api/rewards/reward-version.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/reward-version.ts)
- [apps/web/lib/upstash/record-metatags.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/record-metatags.ts)
- [apps/web/app/api/domains/default/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts)
</details>

## Overview

Dub relies on a multi-tiered caching and edge configuration architecture to minimize database load, maintain high availability during traffic spikes, and ensure ultra-low latency link resolution at the edge. By combining In-Memory LRU caches, Vercel runtime cache layers, and global Upstash Redis instances, the system optimizes read operations and handles failovers seamlessly when remote stores encounter disruptions.
Sources: [apps/web/lib/api/links/cache.ts:15-35](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L15-L35), [apps/web/lib/upstash/redis.ts:9-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L9-L15)

Beyond short link metadata, this infrastructure orchestrates domain lifecycle synchronization, edge-level rate limiting policies, background cache invalidation cron workers, and specialized auxiliary caches for authentication tokens, workspace flags, hostnames, and metatags.
Sources: [apps/web/lib/api/links/cache.ts:44-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L44-L48), [apps/web/app/ee/api/cron/domains/update/route.ts:98-106](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts#L98-L106), [apps/web/lib/upstash/ratelimit-policies.ts:18-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L18-L24), [apps/web/lib/auth/token-cache.ts:4-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L4-L7)

## Redis Client and Global Infrastructure

### Overview

The Upstash Redis infrastructure establishes multiple client instances to isolate critical background operations from standard application traffic. Publicly exported connection handlers initialize clients using environment variables for REST endpoints and authentication tokens, supporting both standard database connections and dedicated global infrastructure layers.
Sources: [apps/web/lib/upstash/redis.ts:1-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L1-L7), [apps/web/lib/upstash/redis.ts:9-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L9-L26)

### Global Connection Configuration

Client initialization checks for specialized global environment variables to determine whether operations should target a secondary Upstash Redis cluster. If `UPSTASH_GLOBAL_REDIS_REST_URL` and `UPSTASH_GLOBAL_REDIS_REST_TOKEN` are both present, `redisConfig` routes connections to the global cluster; otherwise, it falls back to the standard `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`.
Sources: [apps/web/lib/upstash/redis.ts:12-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L12-L23)

Three primary Redis client instances are exported for application usage:
* `redis`: Connects using standard primary environment variables (`UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`).
* `redisGlobal`: Connects using `redisConfig`, targeting global infrastructure (such as `linkCache` and `recordClick`) so that transient cluster failures do not impact unrelated endpoints.
* `redisGlobalWithTimeout`: Extends `redisConfig` by injecting a signal method enforcing an `AbortSignal.timeout(1500)` constraint.
Sources: [apps/web/lib/upstash/redis.ts:4-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L4-L7), [apps/web/lib/upstash/redis.ts:9-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L9-L15), [apps/web/lib/upstash/redis.ts:25-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L25-L30)

> [!NOTE]
> The `redisGlobalWithTimeout` client enforces a strict 1500ms timeout via `AbortSignal.timeout(1500)`, making it suitable for latency-sensitive read paths like `RecordClickCache.get()`.
> Sources: [apps/web/lib/upstash/redis.ts:27-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L27-L30), [apps/web/lib/api/links/record-click-cache.ts:28-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L28-L32)

### Click Caching Operations

#### Overview

The `RecordClickCache` class abstracts click deduplication and persistence through the global Redis clients. Click keys are formulated using domain, link key, and identity hash attributes.
Sources: [apps/web/lib/api/links/record-click-cache.ts:6-11](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L6-L11), [apps/web/lib/api/links/record-click-cache.ts:34-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L34-L36)

#### Call-Chain Execution Walkthrough

When checking or recording a click ID, operations flow through specific methods in the cache layer:
1. `RecordClickCache._createKey()` formats the namespaced Redis key string: `recordClick:${domain}:${key}:${identityHash}`.
Sources: [apps/web/lib/api/links/record-click-cache.ts:34-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L34-L36)
2. `RecordClickCache.set()` invokes `redisGlobal.set()`, passing the generated key, `clickId` value, and an expiration option `ex` set to `CACHE_EXPIRATION` (60 seconds × 60 minutes = 3600 seconds).
Sources: [apps/web/lib/api/links/record-click-cache.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L4-L5), [apps/web/lib/api/links/record-click-cache.ts:13-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L13-L26)
3. `RecordClickCache.get()` invokes `redisGlobalWithTimeout.get<string>()`, querying the store with the 1500ms abort signal boundary enforced.
Sources: [apps/web/lib/api/links/record-click-cache.ts:28-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L28-L32)

### Redis Infrastructure Reference

| Exported Identifier | Underlying Configuration Source | Timeout Signal | Primary Purpose |
| :--- | :--- | :--- | :--- |
| `redis` | `UPSTASH_REDIS_REST_URL` / `TOKEN` | None | Standard application Redis operations |
| `redisGlobal` | `UPSTASH_GLOBAL_REDIS_REST_URL` (with fallback) | None | Global operations (`linkCache`, `recordClick`) |
| `redisGlobalWithTimeout` | `UPSTASH_GLOBAL_REDIS_REST_URL` (with fallback) | `AbortSignal.timeout(1500)` | Time-bounded global read queries (`RecordClickCache.get`) |

Sources: [apps/web/lib/upstash/redis.ts:4-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L4-L7), [apps/web/lib/upstash/redis.ts:12-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/redis.ts#L12-L30), [apps/web/lib/api/links/record-click-cache.ts:28-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L28-L32)

## Link Caching Layer Architecture

### Overview

The `LinkCache` class and its supporting runtime structures manage short link metadata resolution, multi-tier caching, and fallback execution. To mitigate database load during traffic spikes and regional cold starts, short link lookups combine an in-memory `LRUCache` instance (bounded at 10,000 entries with a 5-second TTL), global Upstash Redis storage (`redisGlobal` and `redisGlobalWithTimeout`), Vercel runtime cache (`vercelCache`), and direct PlanetScale database queries via edge connectors (`getLinkViaEdge`).
Sources: [apps/web/lib/api/links/cache.ts:8-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L8-L27), [apps/web/lib/api/links/cache.ts:71-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L71-L136)

### Read Operations and Fallback Mechanics

#### Overview

When a link lookup is requested via `LinkCache.get({ domain, key })`, the execution traverses multiple cache tiers and fallback mechanisms before returning link metadata or throwing a 404 error.
Sources: [apps/web/lib/api/links/cache.ts:71-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L71-L136)

#### Call-Chain Execution Walkthrough

1. `LinkCache.get()` constructs the namespace-aware key `linkcache:${domain}:${key}`.
Sources: [apps/web/lib/api/links/cache.ts:78-78](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L78-L78)
2. It probes the in-memory `linkLRUCache.get(cacheKey)`. If found, it refreshes the LRU entry and returns immediately with `redisFailOver: false`.
Sources: [apps/web/lib/api/links/cache.ts:81-87](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L81-L87)
3. On an LRU miss, it queries global Redis using `redisGlobalWithTimeout.get<RedisLinkProps>(cacheKey)`. If found, it populates the LRU cache and returns with `redisFailOver: false`.
Sources: [apps/web/lib/api/links/cache.ts:93-98](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L93-L98)
4. If Redis throws an error or times out, execution enters the catch block, querying `vercelCache.get(cacheKey)`. If present, it updates the LRU cache and returns with `redisFailOver: true`.
Sources: [apps/web/lib/api/links/cache.ts:105-113](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L105-L113)
5. On a Vercel cache miss, it calls `getLinkViaEdge({ domain, key })`. If no database record is found, it throws an explicit `Error("Link not found.")`. Otherwise, it formats the link via `formatRedisLink()`, writes it to the LRU cache, registers a background write to Vercel cache using `waitUntil()`, and returns with `redisFailOver: true`.
Sources: [apps/web/lib/api/links/cache.ts:117-134](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L117-L134)

> [!WARNING]
> Because LRU caches are unshared across newly spun-up Fluid runtime instances during traffic surges, `LinkCache` falls back to `vercelCache` whenever global Redis is unavailable, preventing stampedes on the underlying database.
> Sources: [apps/web/lib/api/links/cache.ts:23-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L23-L27), [apps/web/lib/api/links/cache.ts:105-113](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L105-L113)

### Write, Deletion, and Expiration Operations

`LinkCache` provides batch and single-item modification routines that synchronize Redis pipelines, LRU entries, and Next.js cache tags.
Sources: [apps/web/lib/api/links/cache.ts:33-70](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L33-L70), [apps/web/lib/api/links/cache.ts:138-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L138-L171)

| Method Signature | Operations Performed | Expiration / TTL | Sources |
| :--- | :--- | :--- | :--- |
| `LinkCache.set(link, options)` | Updates LRU cache, triggers `revalidateTag`, updates Redis, and invalidates Vercel runtime cache. | 24 Hours (`REDIS_CACHE_EXPIRATION`) | [apps/web/lib/api/links/cache.ts:50-69](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L50-L69) |
| `LinkCache.mset(links)` | Pipeline sets multiple links in global Redis and revalidates individual tags. | 24 Hours (`REDIS_CACHE_EXPIRATION`) | [apps/web/lib/api/links/cache.ts:33-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L33-L48) |
| `LinkCache.delete({ domain, key })` | Invalidates Vercel runtime cache via `waitUntil()` and deletes the key from `redisGlobal`. | Immediate deletion | [apps/web/lib/api/links/cache.ts:138-142](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L138-L142) |
| `LinkCache.deleteMany(links)` | Pipelines deletion of multiple link keys across `redisGlobal`. | Immediate deletion | [apps/web/lib/api/links/cache.ts:144-156](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L144-L156) |
| `LinkCache.expireMany(links)` | Pipelines setting key expirations to 1 second for rapid cache invalidation. | 1 Second | [apps/web/lib/api/links/cache.ts:158-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L158-L171) |

> [!TIP]
> The `_createKey()` helper checks domain case-sensitivity using `isCaseSensitiveDomain(domain)`, decoding case-sensitive keys or normalizing case-insensitive keys to lowercase to maintain consistent cache namespaces.
> Sources: [apps/web/lib/api/links/cache.ts:173-179](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L173-L179)

### Design Trade-Offs

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Multi-tier caching (LRU + Redis + Vercel Cache) | Extremely low latency reads and high resilience against regional database or Redis outages. | Increased state synchronization complexity across memory, external stores, and edge runtimes. |
| Pipeline execution for bulk writes (`mset`, `deleteMany`, `expireMany`) | Minimizes network round-trips when modifying multiple cache entries simultaneously. | Requires array length validation checks before executing empty pipelines. |
| In-flight deduplication map in `getLinkViaEdge` | Prevents duplicate concurrent database queries for identical domain and key lookups. | Retains short-lived promise references in memory until resolution settles. |

Sources: [apps/web/lib/api/links/cache.ts:33-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L33-L48), [apps/web/lib/api/links/cache.ts:71-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L71-L136), [apps/web/lib/api/links/cache.ts:144-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/cache.ts#L144-L171), [apps/web/lib/planetscale/get-link-via-edge.ts:41-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L41-L68)

## Edge Middleware Resolution and Fallbacks

### Overview

The Edge Middleware resolves incoming link requests by evaluating domain case sensitivity, normalization rules, and cache presence before falling back to direct edge database execution. `LinkMiddleware` parses the request URL to extract the domain and original key, normalizes keys to lowercase for case-insensitive domains or punycode-encodes them, strips inspect mode suffixes (`+`), and assigns root domain links to `_root`.
Sources: [apps/web/lib/middleware/link.ts:43-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L67)

### Call-Chain Execution Walkthrough

When an incoming request hits the middleware, resolution proceeds through a strict sequence of lookups and fallbacks:
1. `LinkMiddleware` calls `linkCache.get({ domain, key })` to fetch cached metadata and verify `redisFailOver` status.
2. If `cachedLink` is absent, it executes `getLinkViaEdge({ domain, key })`.
3. `getLinkViaEdge` checks `inFlightLinkLookups` Map; if a pending promise exists for the lookup key (`domain:key`), it awaits that promise. Otherwise, it invokes `getLinkViaEdgeHelper`.
4. `getLinkViaEdgeHelper` prepares the query using `isCaseSensitiveDomain(domain)` to decide whether to encode via `encodeKey` or decode and punycode-encode via `punyEncode(safeDecodeURIComponent(key))`, then queries the database via `conn.execute`.
5. If no database record is found and the domain equals `buff.ly`, control falls back to `crawlBitly(req, ev)` which queries the Bitly API, creates a database link via Prisma, records the link, and issues a redirect.
Sources: [apps/web/lib/middleware/link.ts:89-117](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L117), [apps/web/lib/planetscale/get-link-via-edge.ts:10-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L68), [apps/web/lib/middleware/utils/crawl-bitly.ts:20-82](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L82)

### Click Metadata and Caching Helpers

When processing valid links, the middleware evaluates whether to cache click identifiers and record click metadata based on conversion tracking flags, partner status, and tracking URL signatures.
Sources: [apps/web/lib/middleware/link.ts:175-184](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L175-L184)

| Helper / Store | File Path | Key Operations & TTL | Sources |
| :--- | :--- | :--- | :--- |
| `RecordClickCache` | [apps/web/lib/api/links/record-click-cache.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L1-L39) | Caches click IDs in global Redis using keys structured as `recordClick:${domain}:${key}:${identityHash}` with a 1-hour expiration (`60 * 60`). | [apps/web/lib/api/links/record-click-cache.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/record-click-cache.ts#L1-L39) |
| `cacheDeepLinkClickData` | [apps/web/lib/middleware/utils/cache-deeplink-click-data.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/cache-deeplink-click-data.ts#L1-L39) | Stores deep link click payloads (`clickId` and link properties) in Redis keyed by `deepLinkClickCache:${ip}:${link.domain}:${link.key}` with a 1-hour TTL. | [apps/web/lib/middleware/utils/cache-deeplink-click-data.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/cache-deeplink-click-data.ts#L1-L39) |

> [!WARNING]
> During Redis failover states, click tracking routines are explicitly skipped to prevent request timeouts, and cookie lookup or minting for `dubIdCookieName` is bypassed entirely.
> Sources: [apps/web/lib/middleware/link.ts:93-98](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L93-L98), [apps/web/lib/middleware/link.ts:193-211](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L193-L211)

## Domain Configuration and Vercel Synchronization

### Overview

Domain configurations manage routing, branding, and asset settings across workspaces while synchronizing records between primary database stores, Vercel deployments, and edge memory caches. When a domain is created or modified, API route handlers validate constraints, apply plan tier checks, and push configuration changes to external services before updating persistent storage.
Sources: [apps/web/app/api/domains/route.ts:96-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L96-L173), [apps/web/app/api/domains/domain/route.ts:45-64](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L45-L64)

### Edge Domain Retrieval and Caching

To optimize edge execution performance without repeated round-trips to the primary database, domain lookups at the edge leverage an in-memory Least Recently Used (LRU) cache. 

```typescript
const domainLRUCache = new LRUCache<string, EdgeDomainProps>({
  max: 1000,
  ttl: 5 * 60 * 1000, // 5 minutes
});

export const getDomainViaEdge = async (domain: string) => {
  const cached = domainLRUCache.get(domain);
  if (cached) {
    return cached;
  }

  const { rows } =
    (await conn.execute<EdgeDomainProps>(
      "SELECT * FROM Domain WHERE slug = ?",
      [domain],
    )) || {};

  const result =
    rows && Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
  if (result !== null) {
    domainLRUCache.set(domain, result);
  }
  return result;
};
```
Sources: [apps/web/lib/planetscale/get-domain-via-edge.ts:1-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-domain-via-edge.ts#L1-L28)

### Vercel Synchronization and Import Lifecycles

Domain registration invokes environment-specific synchronization with Vercel and manages bulk link transfers during workspace imports. 

When a domain is added via the API, the system checks the `VERCEL` environment variable and registers the domain against Vercel infrastructure, optionally ignoring `domain_already_in_use` responses. During short.io imports, unmanaged source domains are automatically provisioned in the workspace database, synced with Vercel, and initialized with root records in a transaction block.
Sources: [apps/web/app/api/domains/route.ts:163-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L163-L173), [apps/web/app/api/workspaces/idOrSlug/import/short/route.ts:92-125](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/import/short/route.ts#L92-L125)

| Domain API Endpoint | HTTP Method | Action & Sync Behavior | Sources |
| :--- | :--- | :--- | :--- |
| `/api/domains` | `GET` | Retrieves all workspace domains with optional search filters, pagination, and root link mappings. | [apps/web/app/api/domains/route.ts:23-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L23-L94) |
| `/api/domains` | `POST` | Validates limits, parses asset JSON configurations, registers the domain with Vercel if `VERCEL === "1"`, and inserts the domain record. | [apps/web/app/api/domains/route.ts:97-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L97-L173) |
| `/api/domains/[domain]` | `GET` | Fetches a single workspace domain after executing validation and Dub domain checks. | [apps/web/app/api/domains/domain/route.ts:28-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L28-L42) |
| `/api/workspaces/[idOrSlug]/import/short` | `POST` | Discovers external Short.io domains, provisions missing items in Prisma, syncs them to Vercel, and queues cron import jobs. | [apps/web/app/api/workspaces/idOrSlug/import/short/route.ts:79-147](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/import/short/route.ts#L79-L147) |

> [!WARNING]
> When adding domains programmatically, failure to handle Vercel synchronization errors other than `domain_already_in_use` will cause the creation request to abort with an HTTP 422 status response.
> Sources: [apps/web/app/api/domains/route.ts:167-172](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L167-L172)

## Edge Rate Limiting Architecture

### Overview

The rate limiting infrastructure uses Upstash Redis to enforce endpoint-specific consumption policies across authentication, upload, file transfer, and domain management workflows. Policies are typed using `RatelimitPolicy` definitions and referenced by name across application routes.
Sources: [apps/web/lib/upstash/ratelimit-policies.ts:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L1-L16), [apps/web/lib/upstash/index.ts:1-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/index.ts#L1-L5)

### Rate Limit Policy Definitions

The `RATELIMIT_POLICIES` constant maps policy identifiers to specific attempt thresholds, time windows, key prefixes, and custom violation error messages. 

| Policy Identifier | Attempts | Window | Key Prefix | Custom Message / Behavior | Sources |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `login` | 5 | `1 m` | `rl:auth:login` | `too-many-login-attempts` (matched by sign-in page) | [apps/web/lib/upstash/ratelimit-policies.ts:19-24](https://github.com/blade47/dub/blob/HEAD/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/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L26-L30) |
| `emailChangeRequest` | 3 | `24 h` | `rl:auth:email-change` | — | [apps/web/lib/upstash/ratelimit-policies.ts:56-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L56-L60) |
| `emailChangeRequestTarget` | 3 | `24 h` | `rl:auth:email-change:target` | Keyed on target email address | [apps/web/lib/upstash/ratelimit-policies.ts:63-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L63-L67) |
| `workspaceFileUpload` | 20 | `1 h` | `rl:workspace:file:upload` | `Too many file uploads. Please try again later.` (Keyed on workspace + user) | [apps/web/lib/upstash/ratelimit-policies.ts:98-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L98-L104) |
| `reattributeCustomer` | 1 | `1 m` | `rl:customers:reattribute` | Dynamic function incorporating `retryAfter` context | [apps/web/lib/upstash/ratelimit-policies.ts:144-151](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L144-L151) |
| `anonymousLinkCreate` | 10 | `1 d` | `rl:links:create:anonymous` | `Rate limited – you can only create up to 10 links per day without an account.` | [apps/web/lib/upstash/ratelimit-policies.ts:298-305](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L298-L305) |
| `domainSearchAvailability` | 1 | `5 s` | `rl:domains:search-availability` | `Don't DDoS me pls 🥺` | [apps/web/lib/upstash/ratelimit-policies.ts:339-344](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L339-L344) |

Sources: [apps/web/lib/upstash/ratelimit-policies.ts:19-352](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L19-L352)

> [!NOTE]
> Policies such as `socialAccountVerification` are explicitly shared between start and verification routes so that both actions count toward a single unified platform budget.
> Sources: [apps/web/lib/upstash/ratelimit-policies.ts:173-179](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/ratelimit-policies.ts#L173-L179)

## Cache Invalidation and Background Sync

### Overview

Cache invalidation and background synchronization are managed through dedicated cron endpoints and background job processors that purge stale cache keys, synchronize Redis state resources, and process high-volume event streams. Operations such as domain updates, partner data modifications, and discount adjustments rely on batched routines to expire associated short links and maintain consistency between persistent storage and edge caches.
Sources: [apps/web/app/ee/api/cron/domains/update/route.ts:1-121](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts#L1-L121), [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:1-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L1-L42), [apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts:1-210](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts#L1-L210)

### Cron and Background Sync Handlers

Background maintenance tasks execute on regular intervals, utilizing QStash signature verification and concurrency protection locks. The sync scheduler orchestrates Redis resource synchronization across workspace integrations and webhook sets.
Sources: [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:1-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L1-L42), [apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:1-21](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts#L1-L21)

| Handler Route / Module | Schedule / Mechanism | Action Performed | Sources |
| :--- | :--- | :--- | :--- |
| `/api/cron/sync-redis-resources` | Every 5 minutes (`*/5 * * * *`) | Rebuilds Redis sets for click webhook workspaces, cleans redundant link webhooks, and syncs Google Ads installed workspaces. | [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:16-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L16-L23) |
| `/api/cron/cleanup/link-retention` | Once every 12 hours (`0 */12 * * *`) | Deletes expired links exceeding domain `linkRetentionDays` in batches of 100 with pagination. | [apps/web/app/ee/api/cron/cleanup/link-retention/route.ts:13-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/cleanup/link-retention/route.ts#L13-L39) |
| `/api/cron/links/invalidate-for-partners` | QStash triggered POST | Queries program enrollments and associated links for a given `partnerId` and expires them in `linkCache`. | [apps/web/app/ee/api/cron/links/invalidate-for-partners/route.ts:14-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/links/invalidate-for-partners/route.ts#L14-L49) |
| `invalidate-links-for-discounts-job` | Background job handler | Dispatches partner and discount scanning routines to clear Redis caches when resolved link discounts change. | [apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts:42-53](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts#L42-L53) |

Sources: [apps/web/app/ee/api/cron/sync-redis-resources/route.ts:1-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/sync-redis-resources/route.ts#L1-L42), [apps/web/app/ee/api/cron/cleanup/link-retention/route.ts:1-124](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/cleanup/link-retention/route.ts#L1-L124), [apps/web/app/ee/api/cron/links/invalidate-for-partners/route.ts:1-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/links/invalidate-for-partners/route.ts#L1-L54), [apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts:1-210](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts#L1-L210)

### Domain Migration and Batch Invalidation Execution

When migrating links between domains via `/api/cron/domains/update`, processing occurs through a batched lifecycle workflow to prevent timeout and memory exhaustion.

```mermaid
sequenceDiagram
    participant QStash as QStash Cron
    participant Route as /api/cron/domains/update
    participant DB as Prisma DB
    participant Cache as linkCache
    participant Queue as queueDomainUpdate

    QStash->>Route: POST payload (oldDomain, newDomain, startingAfter)
    Route->>DB: prisma.link.findMany({ where: { domain: oldDomain }, take: 100 })
    DB-->>Route: linksToUpdate[]
    Route->>DB: prisma.link.updateMany({ domain: newDomain })
    Route->>DB: prisma.link.findMany (with tags & program enrollment)
    Route->>Cache: linkCache.expireMany(linksToUpdate)
    Route->>Queue: queueDomainUpdate({ startingAfter: lastLinkId, delay: 1 })
    Queue-->>Route: Scheduled next batch messageId
    Route-->>QStash: Log and respond with success
```
Sources: [apps/web/app/ee/api/cron/domains/update/route.ts:21-117](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts#L21-L117)

> [!WARNING]
> Background update routines such as `updateShortLinks` and cache expirations rely on `Promise.allSettled` or chunked iteration to ensure partial network failures in external services do not halt database cursor progression or leave pagination cursors stranded.
> Sources: [apps/web/app/ee/api/cron/domains/update/route.ts:98-105](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts#L98-L105), [apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts:204-206](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/invalidate-links-for-discounts-job.ts#L204-L206)

## Auxiliary Token and Metadata Caching

### Overview

Specialized caching mechanisms support auxiliary workflows across the platform, including restricted authentication tokens, workspace product flags, hostnames, metatags, reward versions, and onboarding states. These stores use Upstash Redis pipelines and key-value methods to maintain TTLs and minimize database pressure for frequently queried auxiliary entities.
Sources: [apps/web/lib/analytics/allowed-hostnames-cache.ts:1-53](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/allowed-hostnames-cache.ts#L1-L53), [apps/web/lib/api/workspaces/workspace-product-cache.ts:1-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/workspace-product-cache.ts#L1-L27), [apps/web/lib/auth/token-cache.ts:1-76](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L1-L76), [apps/web/lib/api/rewards/reward-version.ts:1-50](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/reward-version.ts#L1-L50), [apps/web/lib/upstash/record-metatags.ts:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/record-metatags.ts#L1-L20)

### Auxiliary Cache Configurations and TTLs

| Cache Service / Store | Key Prefix | TTL / Expiration | Source File |
| :--- | :--- | :--- | :--- |
| `AllowedHostnamesCache` | `allowedHostnamesCache` | 7 days (`60 * 60 * 24 * 7`) | [apps/web/lib/analytics/allowed-hostnames-cache.ts:3-4](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/allowed-hostnames-cache.ts#L3-L4) |
| `WorkspaceProductCache` | `workspace:product` | 30 days (`60 * 60 * 24 * 30`) | [apps/web/lib/api/workspaces/workspace-product-cache.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/workspace-product-cache.ts#L4-L5) |
| `TokenCache` | `dubTokenCache` | 24 hours (`60 * 60 * 24`) | [apps/web/lib/auth/token-cache.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L4-L5) |
| Reward Version | `reward-version:{groupId}:{event}` | 24 hours (`24 * 60 * 60`) | [apps/web/lib/api/rewards/reward-version.ts:4-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/reward-version.ts#L4-L14) |
| Onboarding Domain | `onboarding-domain:{workspaceId}` | 15 days (`60 * 60 * 24 * 15`) | [apps/web/app/api/domains/client/saved/route.ts:20-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/client/saved/route.ts#L20-L25) |

Sources: [apps/web/lib/analytics/allowed-hostnames-cache.ts:3-4](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/allowed-hostnames-cache.ts#L3-L4), [apps/web/lib/api/workspaces/workspace-product-cache.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/workspace-product-cache.ts#L4-L5), [apps/web/lib/auth/token-cache.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L4-L5), [apps/web/lib/api/rewards/reward-version.ts:4-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/reward-version.ts#L4-L14), [apps/web/app/api/domains/client/saved/route.ts:20-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/client/saved/route.ts#L20-L25)

### Token and Metatag Management Operations

Restricted and legacy personal tokens use `TokenCache` to serialize parsed token records against hashed keys, executing batch expirations by forcing TTL reductions down to 1 second via Redis pipelines. Metatag generation metrics and failures are tracked using Upstash sorted set increments via `recordMetatags`, routing errors to `metatags-error-zset` and successful domain generations to `metatags-zset`.
Sources: [apps/web/lib/auth/token-cache.ts:31-69](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L31-L69), [apps/web/lib/upstash/record-metatags.ts:8-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/record-metatags.ts#L8-L20)

> [!NOTE]
> When executing `AllowedHostnamesCache.mset` or `TokenCache.expireMany`, empty input arrays short-circuit execution without invoking redis pipeline instructions.
> Sources: [apps/web/lib/analytics/allowed-hostnames-cache.ts:14-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/analytics/allowed-hostnames-cache.ts#L14-L16), [apps/web/lib/auth/token-cache.ts:57-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L57-L60)

## Related

- [Routing and Multitenancy](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/routing-and-multitenancy)
- [Link Resolution and Redirection](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-resolution-and-redirection)


## Sitemap

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