From 6e07fbfa4bbf86509c006c0c164acfef95f4d128 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 16:36:50 +0000 Subject: [PATCH 1/4] perf(rpc): skip redundant Redis backfill and unblock audit writes in link create When beginCachedLinkMutation fails due to Redis being unavailable, the subsequent backfillLinkCache call hits the same dead Redis and blocks for another 1.5s (the LINK_CACHE_OPERATION_DEADLINE_MS timeout) before failing. This adds 1.5s of wasted latency to every link create request during Redis instability. Changes: - Track cacheUnavailable flag when begin-mutation errors on Redis - Skip backfillLinkCache entirely when Redis is known to be down (both in the success path and in the reconciliation path) - Make backfillLinkCache fire-and-forget (unawaited) in the cache-bypass path since the PG row is already committed and backfill errors are already caught internally - Make writeMutationAudit fire-and-forget in runAuditedMutation since appendAuditEvent already has outbox fallback for durability Net effect: eliminates ~1.5s of dead Redis waiting per create request during cache outages, plus removes the sequential audit INSERT from the response path for all tracked mutations. Co-authored-by: iza --- packages/rpc/src/middleware/audit-mutation.ts | 4 ++-- packages/rpc/src/routers/links.ts | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/rpc/src/middleware/audit-mutation.ts b/packages/rpc/src/middleware/audit-mutation.ts index 7d64275d1..6605f3912 100644 --- a/packages/rpc/src/middleware/audit-mutation.ts +++ b/packages/rpc/src/middleware/audit-mutation.ts @@ -123,10 +123,10 @@ export async function runAuditedMutation( try { const result = await fn(); - await writeMutationAudit(context, path, "success"); + writeMutationAudit(context, path, "success"); return result; } catch (error) { - await writeMutationAudit(context, path, getAuditOutcome(error), error); + writeMutationAudit(context, path, getAuditOutcome(error), error); throw error; } } diff --git a/packages/rpc/src/routers/links.ts b/packages/rpc/src/routers/links.ts index 92894c067..736d98499 100644 --- a/packages/rpc/src/routers/links.ts +++ b/packages/rpc/src/routers/links.ts @@ -589,6 +589,7 @@ export const linksRouter = { for (const slug of slugsToTry) { const linkId = randomUUIDv7(); let cacheMutations: LinkCacheMutation[] | null; + let cacheUnavailable = false; try { cacheMutations = await beginLinkCacheMutations([ { id: linkId, mode: "new", slug }, @@ -608,6 +609,7 @@ export const linksRouter = { // slugs. A cache outage must not make random-slug creation // unavailable; redirects read through to PG on cache misses. cacheMutations = []; + cacheUnavailable = true; } if (!cacheMutations) { @@ -669,12 +671,8 @@ export const linksRouter = { { link: toCachedLink(newLink), state: "link" }, "create persisted" ); - } else { - await backfillLinkCache( - slug, - newLink, - "create bypassed cache lease" - ); + } else if (!cacheUnavailable) { + backfillLinkCache(slug, newLink, "create bypassed cache lease"); } invalidateLinkAgentContext(organizationId); @@ -734,8 +732,8 @@ export const linksRouter = { { link: toCachedLink(persistedLink), state: "link" }, "create reconciled after ambiguous database error" ); - } else { - await backfillLinkCache( + } else if (!cacheUnavailable) { + backfillLinkCache( slug, persistedLink, "create reconciled after cache bypass" From 987cd5945713372f2eea8a8a169c91fd66959573 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 16:36:55 +0000 Subject: [PATCH 2/4] docs(rules): add Redis retry and audit write perf lessons Co-authored-by: iza --- .cursor/rules/performance.mdc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cursor/rules/performance.mdc b/.cursor/rules/performance.mdc index 8270b28e1..5dd3bdb75 100644 --- a/.cursor/rules/performance.mdc +++ b/.cursor/rules/performance.mdc @@ -38,6 +38,8 @@ When you discover a new performance improvement, optimization pattern, or fix a - Skip expensive middleware for routes that don't need it — `applyAuthWideEvent` does a session DB lookup on every request; skip it for anonymous `/public/` routes via URL check in `onBeforeHandle` - Use `@elysiajs/server-timing` to profile per-phase durations (CORS, beforeHandle, handle, afterHandle) without manual instrumentation - Elysia's JIT compiler is on by default (`precompile: true`) — no extra config needed +- When a Redis call fails (link cache or cacheable), do not retry the same Redis connection in the same request for non-critical work (e.g. cache backfill after a failed begin-mutation) — the timeout penalty (1.5s per call) compounds across sequential attempts +- `runAuditedMutation` audit writes are fire-and-forget after success; `appendAuditEvent` has an outbox fallback so blocking the response on the INSERT is unnecessary latency ## Database queries From 16a8dbc22b3ec31141876acda3c81c75970b057b Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:09:23 +0300 Subject: [PATCH 3/4] refactor(rpc): delete unreachable link cache backfill paths --- packages/rpc/src/routers/links.ts | 32 ------------------------------- 1 file changed, 32 deletions(-) diff --git a/packages/rpc/src/routers/links.ts b/packages/rpc/src/routers/links.ts index 736d98499..49c723566 100644 --- a/packages/rpc/src/routers/links.ts +++ b/packages/rpc/src/routers/links.ts @@ -19,7 +19,6 @@ import { type CachedLinkMutationNext, finishCachedLinkMutation, invalidateAgentContextSnapshotsForOwner, - setCachedLinkIfAbsent, } from "@databuddy/redis"; import { isDeepLinkTarget } from "@databuddy/shared/constants/deep-link-apps"; import { randomUUIDv7 } from "bun"; @@ -281,27 +280,6 @@ async function finishLinkCacheMutation( return false; } -async function backfillLinkCache( - slug: string, - link: CacheableLink, - reason: string -): Promise { - try { - if (await setCachedLinkIfAbsent(slug, toCachedLink(link))) { - return; - } - logger.warn( - { linkId: link.id, slug, reason }, - "Link cache backfill did not replace an existing entry" - ); - } catch (error) { - logger.error( - { linkId: link.id, slug, reason, ...getErrorLogFields(error) }, - "Failed to backfill link cache" - ); - } -} - async function tombstoneLinkCacheMutations( mutations: LinkCacheMutation[], reason: string @@ -589,7 +567,6 @@ export const linksRouter = { for (const slug of slugsToTry) { const linkId = randomUUIDv7(); let cacheMutations: LinkCacheMutation[] | null; - let cacheUnavailable = false; try { cacheMutations = await beginLinkCacheMutations([ { id: linkId, mode: "new", slug }, @@ -609,7 +586,6 @@ export const linksRouter = { // slugs. A cache outage must not make random-slug creation // unavailable; redirects read through to PG on cache misses. cacheMutations = []; - cacheUnavailable = true; } if (!cacheMutations) { @@ -671,8 +647,6 @@ export const linksRouter = { { link: toCachedLink(newLink), state: "link" }, "create persisted" ); - } else if (!cacheUnavailable) { - backfillLinkCache(slug, newLink, "create bypassed cache lease"); } invalidateLinkAgentContext(organizationId); @@ -732,12 +706,6 @@ export const linksRouter = { { link: toCachedLink(persistedLink), state: "link" }, "create reconciled after ambiguous database error" ); - } else if (!cacheUnavailable) { - backfillLinkCache( - slug, - persistedLink, - "create reconciled after cache bypass" - ); } invalidateLinkAgentContext(organizationId); return persistedLink; From 9bfee91247a50d7463254a0dcef6133f4afe377c Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:09:42 +0300 Subject: [PATCH 4/4] perf(redis): fail fast on link cache commands after a Redis failure --- .cursor/rules/performance.mdc | 2 +- packages/redis/000-redis.test.ts | 35 ++++++++++++++++++++++++++++++-- packages/redis/redis.ts | 13 +++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.cursor/rules/performance.mdc b/.cursor/rules/performance.mdc index 5dd3bdb75..e153981ef 100644 --- a/.cursor/rules/performance.mdc +++ b/.cursor/rules/performance.mdc @@ -38,7 +38,7 @@ When you discover a new performance improvement, optimization pattern, or fix a - Skip expensive middleware for routes that don't need it — `applyAuthWideEvent` does a session DB lookup on every request; skip it for anonymous `/public/` routes via URL check in `onBeforeHandle` - Use `@elysiajs/server-timing` to profile per-phase durations (CORS, beforeHandle, handle, afterHandle) without manual instrumentation - Elysia's JIT compiler is on by default (`precompile: true`) — no extra config needed -- When a Redis call fails (link cache or cacheable), do not retry the same Redis connection in the same request for non-critical work (e.g. cache backfill after a failed begin-mutation) — the timeout penalty (1.5s per call) compounds across sequential attempts +- Redis clients fail fast after a failure instead of paying the timeout per call: `cacheable` skips Redis for 30s after a timeout, and `runLinkCacheCommand` rejects immediately for 5s after a failed operation. Sequential deadline penalties (1.5–2s per call) must never compound within one request - `runAuditedMutation` audit writes are fire-and-forget after success; `appendAuditEvent` has an outbox fallback so blocking the response on the INSERT is unnecessary latency ## Database queries diff --git a/packages/redis/000-redis.test.ts b/packages/redis/000-redis.test.ts index 3d2316649..b9b61a8fd 100644 --- a/packages/redis/000-redis.test.ts +++ b/packages/redis/000-redis.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, it } from "bun:test"; +import { afterAll, describe, expect, it, mock } from "bun:test"; import { createLinkCacheRedisConnectionOptions, createRateLimitRedisConnectionOptions, @@ -8,7 +8,9 @@ import { process.env.REDIS_URL = "redis://test-host:6379"; -const { getRedisCache, shutdownRedis } = await import("./redis"); +const { getRedisCache, runLinkCacheCommand, shutdownRedis } = await import( + "./redis" +); describe("redis", () => { describe("connection options", () => { @@ -106,4 +108,33 @@ describe("redis", () => { second.disconnect(); }); }); + + describe("link cache fail-fast", () => { + afterAll(async () => { + await shutdownRedis(); + }); + + it("rejects immediately after a recent failure without running the operation", async () => { + await expect( + runLinkCacheCommand(async () => "unreachable") + ).rejects.toThrow(); + + const operation = mock(async () => "value"); + const startedAt = performance.now(); + await expect(runLinkCacheCommand(operation)).rejects.toThrow( + "failing fast" + ); + expect(performance.now() - startedAt).toBeLessThan(100); + expect(operation).not.toHaveBeenCalled(); + }); + + it("probes again after shutdown resets the fail-fast window", async () => { + await shutdownRedis(); + const error = await runLinkCacheCommand(async () => "value").catch( + (caught: Error) => caught + ); + expect(error).toBeInstanceOf(Error); + expect(error.message).not.toContain("failing fast"); + }); + }); }); diff --git a/packages/redis/redis.ts b/packages/redis/redis.ts index 64b9d7711..366f7270a 100644 --- a/packages/redis/redis.ts +++ b/packages/redis/redis.ts @@ -15,9 +15,12 @@ let shutdownHooksRegistered = false; const LINK_CACHE_CONNECT_DEADLINE_MS = 1250; export const LINK_CACHE_OPERATION_DEADLINE_MS = 1500; +const LINK_CACHE_FAIL_FAST_WINDOW_MS = 5000; const RATE_LIMIT_CONNECT_DEADLINE_MS = 1250; export const RATE_LIMIT_OPERATION_DEADLINE_MS = 1500; +let linkCacheFailFastUntil = 0; + function withDeadline( operation: Promise, timeoutMs: number, @@ -153,6 +156,10 @@ export function runRateLimitCommand( async function runLinkCacheRedisCommand( operation: (redis: Redis) => Promise ): Promise { + if (Date.now() < linkCacheFailFastUntil) { + throw new Error("Link cache is failing fast after a recent Redis failure"); + } + let instance: Redis | null = null; const command = getLinkCacheRedis().then((redis) => { instance = redis; @@ -160,12 +167,15 @@ async function runLinkCacheRedisCommand( }); try { - return await withDeadline( + const result = await withDeadline( command, LINK_CACHE_OPERATION_DEADLINE_MS, `Link cache operation exceeded ${LINK_CACHE_OPERATION_DEADLINE_MS}ms` ); + linkCacheFailFastUntil = 0; + return result; } catch (error) { + linkCacheFailFastUntil = Date.now() + LINK_CACHE_FAIL_FAST_WINDOW_MS; if (instance) { discardLinkCacheRedis(instance); } @@ -197,6 +207,7 @@ async function runRateLimitRedisCommand( } export async function shutdownRedis() { + linkCacheFailFastUntil = 0; const linkCacheInstance = linkCacheRedisInstance; linkCacheRedisInstance = null; linkCacheConnectPromise = null;