Bound the gateway request path: caching, backpressure and pagination - #238
Merged
anilguleroglu merged 1 commit intoAug 22, 2026
Conversation
A second pass over the load path, this time on what a request actually touches on its way through the gateway — Model Hub, Knowledge Engine and the work each of them queues behind the response. - The provider row behind a runtime is cached. The SDK client was already pooled, but the row describing it was re-read on every call, so one Knowledge Engine query — embedding, then vector store, then reranker — spent three uncached reads before doing any work. What is cached is the row as stored: credentials stay sealed and are decrypted per call, never cached in the clear, and an operator who would rather keep that ciphertext out of the cache backend can set the TTL to 0. - Guardrail configs are cached. A guarded model resolved its guardrail on the request path, twice over when both an input and an output guardrail were configured, and the lookup went to the database every time. - Parent-window retrieval is bounded. Expansion reads a whole document per matched chunk — up to 250k characters inline, or a full object download for anything larger — and it did so for every match of every query, with no cap, no ceiling on concurrency and no reuse between queries. A query against twenty documents pulled megabytes of text into heap to compute windows it had computed a second earlier. Now capped, bounded in flight, and cached, with a log line when the cap truncates rather than silently returning narrower context. - Background work is queued rather than started on arrival. Every usage write, audit row and trace payload became a live promise in an unbounded set, so the backlog grew with the request rate and competed with foreground queries for the same small pool while holding megabyte trace bodies in heap. A bounded number of runners drains a queue; only telemetry is shed under pressure, and usage, billing and audit writes are always kept. - Rate-limit counters are written as one batch. A guarded request touches a counter per metric per window — up to 25 — and issuing them separately let a single caller hold every connection in the pool while its own limits were checked. MongoDB gets one bulkWrite plus one read-back, Redis one pipeline; the in-process providers keep looping, since there is no round trip there to save. - List endpoints that grow with usage take limit/offset. Knowledge Engine documents, agents, tools and prompts returned whole collections. They now return a bounded page with pagination metadata. Where a count query filters identically to its list it reports a total; elsewhere it reads one row past the window instead, which is cheaper and cannot drift. `/client/v1/models` is deliberately left unpaged: it serves the OpenAI-compatible discovery list, which clients expect whole. Adds 31 tests over the caches, the queue's ceiling and shedding, the batched counters, the parent-window cap and the pagination helpers, plus a cache reset in the shared test setup — request-path caches now outlive a single case, and one test's fixtures must not answer the next one's reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ltao83ZEHPKrpAQZv3n5z1
anilguleroglu
merged commit Aug 22, 2026
d59c31c
into
claude/console-packages-load-analysis-d5tb0f
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A second pass over the load path, this time on what a request actually touches on its way through the gateway — Model Hub, Knowledge Engine, and the work each of them queues behind the response. Every item here is a place with no ceiling rather than a wrong one: a lookup repeated per call, a fan-out sized by user data, a backlog that grows with the request rate, a list that returns a whole collection.
Stacked on #237 — it shares files with that branch (
quotaGuard,ragService), so basing it onmainwould only manufacture conflicts. Review after #237; it retargets tomaincleanly once that merges.Changes
Provider rows are cached (
PROVIDER_RECORD_CACHE_TTL_SECONDS, default 60). The SDK client was already pooled, but the row describing it was re-read on every call — one Knowledge Engine query (embedding → vector store → reranker) spent three uncached reads before doing any work. What's cached is the row as stored:credentialsEncstays sealed underPROVIDER_ENCRYPTION_SECRET, decryption stays per call, plaintext credentials are never cached (test asserts this). Provider create/update/delete invalidate every affected project key; TTL0keeps the ciphertext out of the cache backend entirely.Guardrail configs are cached (
GUARDRAIL_CONFIG_CACHE_TTL_SECONDS, default 30). A guarded model resolved its guardrail on the request path — twice when both an input and an output guardrail are set — and went to the database each time. Guardrail edits invalidate.Parent-window retrieval is bounded — the sharpest one. Expansion reads a whole document per matched chunk (up to
INLINE_SOURCE_MAX_CHARS= 250k inline, or a full object download beyond that), and did so for every match of every query: no cap, unboundedPromise.all, no reuse. AtopK-20 query spanning 20 documents pulled megabytes of text into heap to compute windows it had computed a second earlier. Now capped (RAG_PARENT_SOURCE_MAX_DOCUMENTS, 25), bounded in flight (…_CONCURRENCY, 4), and cached (…_CACHE_TTL_SECONDS, 60; only sources under…_CACHE_MAX_CHARSare cached, so the fix doesn't trade read amplification for a memory problem). When the cap truncates it logs — the remaining matches keep their chunk text rather than silently returning narrower context.Background work is queued, not started on arrival. Every usage write, audit row and trace payload became a live promise in an unbounded
Set: the backlog grew with the request rate, competed with foreground queries for the same 10-connection pool, and held trace bodies (up toTRACING_MAX_BODY_SIZE_MB) in heap until it drained. Now a bounded number of runners (ASYNC_TASK_MAX_CONCURRENT, 32) drains a queue. Shedding is opt-in per call site: telemetry (trace ingest,token-last-used) isdroppableand sheds oldest-first pastASYNC_TASK_MAX_QUEUED; usage, billing and audit writes are always kept — losing them corrupts records the product bills and answers audits from.Rate-limit counters are written as one batch. A guarded request touches a counter per metric per window — up to 25 — and issuing them separately let one caller hold every pool connection while its own limits were checked. New
incrementRateLimits/incrementCounterson both contracts: MongoDB does onebulkWrite+ one read-back, Redis one pipeline, the in-process providers keep looping (no round trip there to save). Behaviour note: the batch now fails as a unit where individual counters used to fail one at a time — "enforcement unavailable" is the same answer either way, and a partially-applied set would leave the windows disagreeing about how much of the request was counted.Pagination on the list endpoints that grow with usage. Knowledge Engine documents (client + dashboard), agents, tools and prompts returned whole collections; they now take
limit/offsetthrough a shared helper (default 100, max 500) and returnpaginationmetadata. Where a count query filters identically to its list it reportstotal—countRagDocumentswas realigned to matchlistRagDocumentsfor exactly this reason, since a total derived from different filters is worse than none. Elsewhere it reads one row past the window (takePage), which is cheaper and cannot drift.Deliberately not paged:
/client/v1/modelsserves the OpenAI-compatible discovery list, which SDK clients expect whole. Batches already had a bounded default (50, max 500). The remaining list endpoints (projects, members, license, providers, budgets, automations) are admin-managed and bounded by configuration rather than usage — say the word and I'll extend the same helper to them.pagination.hasMore/totalmake the truncation visible, and both are documented inopenapi.yaml.Validation
npm run lint— 0 errors (164 pre-existing warnings)npm run test— 3694 passed, 0 failures. 5 test files fail to start becausemongodb-memory-servercannot download its binary in this sandbox (403/502 fromfastdl.mongodb.org); identical on a clean checkout, and the[sqlite]halves of the same parity suites pass, which is what exercises the new SQLite pagination.npm run buildnpm run docs:buildnpm run test:endpoints— 217 uncovered / 137 new, byte-identical to the base branch.31 new tests: the provider and guardrail caches (including that nothing decrypted is cached), the background queue's ceiling, shedding order and never-shed guarantee, the batched counters (one call, correct rejection, fail-closed, provider routing), the parent-window cap, and the pagination helpers. The shared test setup now clears the core cache between cases — request-path caches outlive a single test in one process, and one case's fixtures must not answer the next one's reads.
Release Notes
.env.example(11 new settings),openapi.yaml(pagination params on four endpoints)Generated by Claude Code