Skip to content

fix(cache): hotpath db queries caching - #2662

Merged
steebchen merged 3 commits into
mainfrom
fix/cache-hotpath-db-queries
Jun 12, 2026
Merged

steebchen merged 3 commits into
mainfrom
fix/cache-hotpath-db-queries

Conversation

@steebchen

@steebchen steebchen commented Jun 12, 2026 •

Copy link
Copy Markdown
Member

Two hot-path lookups ran on every gateway request instead of being served from the Drizzle/Redis cache, hammering Postgres under load:

  • getEffectiveRateLimit used the uncached db client, so the rate_limit query hit Postgres on every request (swrWrap only mirrors to Redis on DB error, not on the happy path). Switch to the cached cdb client; the WHERE clause is time-independent so the cache key is stable.

  • getEffectiveDiscount put now (new Date()) in the SQL WHERE clause. The cached client keys on hashQuery(sql, params), so a per-request millisecond timestamp made the key unique every call → 0% cache hit. Keep the SQL time-independent and evaluate expiry in JS, mirroring the existing findEffectiveDiscount fix.

Verification

Measured e2e against a local gateway (Postgres statement logging, repeated requests with unique prompts so the response cache can't mask the path):

  • A warm request now does 0 Postgres reads (was ~14).
  • A 60-request concurrent burst hit Postgres only 3 times total (cold-cache races) instead of ~840.

Regression guard

Adds apps/gateway/src/chat-cache-no-db.e2e.ts: it warms the gateway caches, then spies on the shared pg pool during a fresh chat completion and asserts zero SELECTs reach the cached metadata tables (api_key, project, organization, provider_key, rate_limit, discount, routing metrics, …). Both the cached (cdb) and uncached (db) clients share this pool, so the spy catches any query that actually reaches Postgres — protecting against both regression vectors: an unstable cache key (per-request Date param) and switching a hot lookup back to the uncached client. Confirmed the test fails when either bug is reintroduced.

Also adds API_URL/MODEL env overrides to scripts/loadtest.mjs so the load test can target a local gateway.

Summary by CodeRabbit

  • Improvements

    • Rate-limit lookups now use a cached backend for faster, more stable lookups.
    • Discount expiry filtering now evaluates expiry at runtime before use.
  • Tests

    • Added end-to-end caching verification to ensure cached reads don’t hit the database once warm.
    • Updated unit tests to reflect changed database client interactions.
  • Chores

    • Added a CLI load-testing script to measure throughput, latency, and error breakdowns.

steebchen and others added 2 commits June 12, 2026 23:09
Two hot-path lookups ran on every gateway request instead of being
served from the Drizzle/Redis cache, hammering Postgres under load:

- getEffectiveRateLimit used the uncached `db` client, so the rate_limit
  query hit Postgres on every request (swrWrap only mirrors to Redis on
  DB error, not on the happy path). Switch to the cached `cdb` client;
  the WHERE clause is time-independent so the cache key is stable.

- getEffectiveDiscount put `now` (new Date()) in the SQL WHERE clause.
  The cached client keys on hashQuery(sql, params), so a per-request
  millisecond timestamp made the key unique every call -> 0% cache hit.
  Keep the SQL time-independent and evaluate expiry in JS, mirroring the
  existing findEffectiveDiscount fix.

Verified e2e against a local gateway: a warm request now does 0 Postgres
reads (was ~14), and a 60-request concurrent burst hit Postgres only 3
times total (cold-cache races) instead of ~840.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow pointing the load test at a local gateway and varying the model
without editing the script. Also move the API key to an env var and add
categorized error reporting (per-category counts + sampled messages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Discount expiry filtering was moved from SQL to JavaScript, rate-limit lookups now use the cached DB client with updated tests, a Node.js load-test CLI was added, and an e2e verifies chat completions metadata reads are served from cache.

Changes

Database Query Optimization

Layer / File(s) Summary
Discount expiry filtering in JavaScript
packages/db/src/discount-helpers.ts
getEffectiveDiscount queries discounts including expiresAt and excludes expired rows in JavaScript using Date.now() and defensive timestamp conversion instead of a SQL expiry predicate.
Rate-limit caching via cached database client
packages/db/src/rate-limit-helpers.ts, packages/db/src/rate-limit-helpers.spec.ts
getEffectiveRateLimit now imports/uses the cached cdb client (JSDoc updated); tests and mocks were updated to mock/configure cdb.select rather than db.select.

Load Testing CLI

Layer / File(s) Summary
Load test CLI script
scripts/loadtest.mjs
Adds a standalone Node.js CLI for configurable-rate load testing against the chat completions endpoint, collecting per-request latency, success/failure counts, error categorization/sampling, and printing summary statistics after the run.

Chat completions cache e2e

Layer / File(s) Summary
Local mock upstream server
apps/gateway/src/chat-cache-no-db.e2e.ts
Adds a Hono-based mock upstream that returns chat completions for the e2e test.
Cache-read filtering helpers
apps/gateway/src/chat-cache-no-db.e2e.ts
Defines metadata tables that must be served from cache and helpers to extract/filter pooled SQL SELECT statements for those tables.
Request helpers and test lifecycle
apps/gateway/src/chat-cache-no-db.e2e.ts
Implements gateway request helper and test setup/teardown: clear caches, truncate/seed DB tables, start/stop mock upstream.
Measured-request assertion
apps/gateway/src/chat-cache-no-db.e2e.ts
Performs warm-up requests, then measures a unique-prompt request while spying on pool.query, asserting no cached-table SELECTs hit Postgres.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: moving hot-path database queries to use caching instead of hitting Postgres on every request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cache-hotpath-db-queries

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot changed the title Fix/cache hotpath db queries fix(cache): hotpath db queries caching Jun 12, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f8a9ffcbb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +174 to 175
const rateLimits = await cdb
.select({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate rate-limit cache on admin mutations

When an admin creates or deletes a rate limit to stop traffic, this cached lookup can keep returning the previous result because the admin handlers mutate tables.rateLimit through the uncached db client (for example apps/api/src/routes/admin.ts uses db.insert/db.delete for these routes), so RedisCache.onMutate is not invoked for the Drizzle cache. A gateway that has already cached “no limit” for an org/provider/model will continue allowing requests until the cache entry expires, regressing the previous immediate enforcement behavior for emergency throttles; either invalidate this cache from the admin mutations or keep this lookup uncached for enforcement-sensitive changes.

Useful? React with 👍 / 👎.

Add an e2e regression test that warms the gateway caches, then spies on
the shared pg pool during a fresh chat completion and asserts zero SELECTs
hit the cached metadata tables (api_key, project, organization, provider_key,
rate_limit, discount, routing metrics, ...). Both the cached (cdb) and
uncached (db) clients share this pool, so the spy catches any query that
actually reaches Postgres — protecting against both regression vectors:
an unstable cache key (per-request Date param) and switching a hot lookup
to the uncached client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/gateway/src/chat-cache-no-db.e2e.ts`:
- Around line 81-89: cachedTableReads only matches the exact substring from
"<table>" and misses variants like schema-qualified ("public"."api_key"), joins,
aliases, or unquoted identifiers; update cachedTableReads to build a pattern
from CACHED_READ_TABLES that tests for table occurrences following SQL keywords
(e.g., FROM, JOIN, INTO) with optional schema qualification and optional quoting
or unquoted identifiers and word boundaries, and use that pattern against the
lowered SQL to return true for any of those variants; reference the
cachedTableReads function and the CACHED_READ_TABLES constant when making this
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 375e4074-0cac-4cc3-965b-80aef278a4e0

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8a9ff and 7e8bd82.

📒 Files selected for processing (1)
  • apps/gateway/src/chat-cache-no-db.e2e.ts

Comment thread apps/gateway/src/chat-cache-no-db.e2e.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e8bd82c27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// (hot, per-provider-candidate) lookup would query Postgres on every
// request. Keeping the SQL time-independent lets the cache key stay stable
// while expiry is still evaluated fresh on each call.
const rows = await cdb

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate discount cache on admin changes

With this lookup now using a stable cdb cache key, admin-created or deleted discounts can be ignored until the 60s Drizzle cache TTL expires, because the admin discount routes mutate tables.discount through the uncached db client (apps/api/src/routes/admin.ts inserts/deletes at lines 3165/3183 and 3273/3291), so RedisCache.onMutate is not invoked. In production this means a newly added discount may not be applied, or a removed discount may keep affecting gateway pricing/cost accounting, for requests whose org/provider/model tuple was already warmed in cache; invalidate the discount cache on these mutations or keep this enforcement/pricing lookup uncached.

Useful? React with 👍 / 👎.

@steebchen
steebchen merged commit 1cea5d9 into main Jun 12, 2026
18 of 19 checks passed
@steebchen
steebchen deleted the fix/cache-hotpath-db-queries branch June 12, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant