Conversation
Replace per-module Keyv factory functions with a single shared instance and optimize cache writes using batch setMany operations. - Add shared cache.ts with single Keyv client for all REST endpoints - Remove createKeyv factories from list, reports, snapshot, timeseries - Switch refresh scripts to batch setMany for fewer Redis round-trips - Consolidate timeseries into one key per vault (all labels together) - Remove manual JSON.stringify/parse in favor of Keyv serialization - Ensure keyv.disconnect() on refresh script exit Co-Authored-By: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
murderteeth
left a comment
There was a problem hiding this comment.
Review: Improve rest cache implementation
1. (keyv as any).setMany(entries) — unsafe cast used 4 times
createKeyv() returns a Keyv v5.5.5 instance that has setMany, but the root keyv package is v4.5.4 — TypeScript resolves to the v4 types. The as any cast works at runtime but silently bypasses type checking on the entry format.
Either upgrade root keyv to v5 so types align, or centralize the cast behind a typed wrapper in cache.ts so callers get type safety.
2. List key format changed — this is a breaking change
The old createListsKeyv('list:vaults') passed a namespace, but createKeyv internally sets useKeyPrefix: false, so the namespace was never applied. Old Redis keys were bare: all, 1, 137, etc. New keys are list:vaults:all, list:vaults:1, etc.
This is actually an improvement (prevents collisions), but it's a breaking change that isn't mentioned in the PR description. Old cached data becomes orphaned and invisible to the new code.
Please add a rollout procedure to the PR description. How do we deploy this without a window where endpoints return 404? Do refresh scripts need to run first? Do old keys need cleanup? This should be spelled out before merge.
3. Hold off on timeseries consolidation
Let's not change the timeseries data model in this PR. The setMany batch writes alone should give us a significant reduction in Upstash usage. Changing the key structure (per-label → per-vault) is a separate concern with its own tradeoffs around value size. Let's measure the setMany improvement first, then decide if consolidation is needed.
Please revert the timeseries consolidation and keep the existing per-label key format. Only apply setMany batching to the current key structure.
4. Test plan needs work
The test plan currently says "Manual: Run each refresh script and verify data is cached correctly." That's not reviewable — I can't tell from that whether you've actually tested this or what "correctly" means.
Please update the test plan with:
- Step-by-step commands a reviewer can copy-paste to verify locally (including any infrastructure like Redis)
- Actual endpoint URLs with example parameters so a reviewer can curl them after running refresh scripts
Also — since the whole motivation is Upstash method calls and bandwidth, please test this against an Upstash test instance and report before/after metrics in a comment. We need to see the actual improvement before merging, especially if we're changing key formats.
|
I have not got used to the namespace thing on keyv, that got very well through the cracks and was the last thing i got attention to, i reverted the timeseries, fair enough that though. |
|
i got asking myself now if that's worth adding a ttl to the keys of 16 minutes at least, so it can be disposed and allow for a more flexible setup here. Given its updated every 15 minutes. |
murderteeth
left a comment
There was a problem hiding this comment.
Review: Improve REST cache implementation
Overall the consolidation of per-module Keyv factories into a shared cache.ts factory and the switch to batch setMany writes are solid improvements. However, there are a few issues worth addressing before merging.
Bug: console.timeEnd label mismatch in list refresh
packages/web/app/api/rest/list/refresh.ts:7 sets console.time('refresh list:vaults') but line 30 calls console.timeEnd('refresh'). The labels don't match — Node will print a warning and won't show elapsed time.
Concern: connectionTimeoutMillis 5s → 50s (10x increase)
packages/web/app/api/db/index.ts changed from 5_000 to 50_000. This is a 10x increase to the Postgres connection timeout, not mentioned anywhere in the PR description. 50 seconds is extremely long — if Postgres is unreachable, requests will hang for nearly a minute before failing. Was this intentional, or a typo (extra 0)?
Inconsistency: Timeseries still double-serializes
All other refresh scripts (list, reports, snapshot) now correctly pass raw objects to keyv.set/setMany and let Keyv v5 handle serialization natively. But refresh-timeseries.ts:41 still wraps values in JSON.stringify(minimal) before passing to setMany. And the timeseries route (route.ts:57) still manually JSON.parses the result.
This means timeseries data gets double-serialized: once by the explicit JSON.stringify, then again by Keyv's built-in @keyv/serialize. On read, Keyv deserializes the outer layer, returning a JSON string that the route then has to JSON.parse again. Should align with the other modules by passing raw objects and removing the manual parse on the route side.
Stale PR description (timeseries consolidation was reverted)
Per the author's comment, the timeseries key consolidation (one key per vault instead of per-label) was reverted due to namespace issues. However, the PR description and risk section still reference this change:
"Consolidates timeseries data into one key per vault (all labels together) instead of one key per label"
"Redis key format changed for timeseries: Keys go from
timeseries:{label}:{chainId}:{address}totimeseries:{chainId}:{address}"
These should be removed/updated to avoid confusing reviewers.
Re: TTL on cache keys
Responding to the author's question about adding a 16-minute TTL — the existing stale-while-revalidate on the HTTP responses is a CDN/browser-level cache setting, not Redis-level, so it wouldn't protect against stale Redis data if refresh scripts fail. However, adding a Redis TTL could make things worse: if a key expires before the next refresh cycle, routes would return 404 instead of slightly stale data. Serving old data is generally preferable to serving nothing.
Nit: Not actually a singleton
The PR summary says "single shared Keyv instance" but cache.ts exports a factory function createKeyvClient() that creates a new Keyv connection each time it's called. Every route file and refresh script creates its own instance at module level. This is correct architecturally (shared config, separate connections), but the description is slightly misleading.
Summary
| Issue | Severity | File |
|---|---|---|
console.timeEnd label mismatch |
Bug | list/refresh.ts |
connectionTimeoutMillis 50s |
Likely typo | db/index.ts |
| Timeseries double serialization | Inconsistency | timeseries/refresh-timeseries.ts, timeseries/.../route.ts |
| PR description stale after timeseries revert | Docs | PR description |
| "Singleton" description | Nit | PR description |
The core refactor (shared factory, setMany batching, removing manual JSON parse) is a clean improvement. The items above are mostly quick fixes.
|
Reiterating from my earlier review — the test plan should be updated to make this PR easier to review. Specifically:
|
|
From my earlier review — the list key format breaking change still needs to be addressed. The Can you verify by checking the actual keys in Redis before and after? Something like: # before merging, check current key format
redis-cli --scan --pattern 'list:vaults*'If the format did change, please document the rollout procedure in the PR description (e.g. run refresh scripts immediately after deploy, or flush old keys). |
Yeah i saw a space in between the keys in redis insigh, but didnt know about this command, ill try that! |
murderteeth
left a comment
There was a problem hiding this comment.
Three items from the previous review still need attention before this can merge:
-
connectionTimeoutMillis: 50_000(packages/web/app/api/db/index.ts:17) — The 10x increase from 5s to 50s was flagged as a likely typo and hasn't been addressed. If intentional, please explain the reasoning. -
Key format change — Can you confirm whether the keyv v5 upgrade changes the key format in Redis? (e.g.
list:vaults::1vslist:vaults:1). If it does, please document a rollout procedure in the PR description so endpoints don't return 404 between deploy and the next refresh cycle. -
Upstash before/after metrics — Since reducing method calls and bandwidth is the primary motivation, it would help to see actual numbers from a test run before merging.
|
Yep, i got side tracked by some personal stuff on friday, gonna go back into this! |
| await keyv.set(cacheKey, JSON.stringify(minimal)) | ||
| entries.push({ | ||
| key: getTimeseriesKey(label, vault.chainId, addressLower), | ||
| value: minimal, |
There was a problem hiding this comment.
is this a breaking change then? if the format changes
There was a problem hiding this comment.
Yeah that's a fair point, im adding to the thread. But i also wanna confirm some stats before. I created a new instance and set the keys as it is rn as plain json, ill let it sit for an hour to get the most accurate stats.
did the branch numbers ever update? i'm concerned the storage size is so much smaller |




Summary
Replaces per-module Keyv factory functions (
createListsKeyv,createReportsKeyv,createSnapshotKeyv,createTimeseriesKeyv) with a sharedcreateKeyvClient()factory incache.ts. Optimizes refresh scripts to use batchsetManywrites instead of individualsetcalls, reducing Redis round-trips and Upstash method call / bandwidth usage. Removes manualJSON.stringify/JSON.parsein favor of native Keyv v5 serialization.Changes
cache.ts— New shared factory function that centralizes Redis URL and namespace configredis.ts— Stripped down to only export key helper functions (factory functions removed)refresh*.ts— BatchsetManywrites instead of individualsetcalls per vault/chain; properkeyv.disconnect()on exitroute.ts— Simplified reads; Keyv handles deserialization natively, no more manualJSON.parseBreaking Change
This PR changes how values are stored in Redis. Previously, values were manually
JSON.stringify'd before being passed to Keyv, resulting in double-serialized strings in Redis (string-escaped JSON). Now, raw JSON objects are passed to Keyv, which handles serialization internally using@keyv/serialize. This means the stored format changes from double-encoded strings to properly serialized JSON.Existing cached keys are incompatible with the new format. After merging, the read paths will fail to deserialize old values correctly (or return malformed data).
Post-merge steps
Flush all REST cache keys from Upstash:
redis-cli -u $REST_CACHE_REDIS_URL FLUSHALLRun all refresh jobs to repopulate the cache with the new format
How to review
packages/web/app/api/rest/cache.ts— the shared Keyv factoryredis.tsfiles (list, reports, snapshot, timeseries) — now only export key helpersrefresh*.tsfiles — batchsetManywrites andkeyv.disconnect()on exitroute.tsfiles — simplified reads without manual JSON parsingTest plan
1. Refresh caches
2. Verify keys landed in Redis
redis-cli --scan --pattern 'list:vaults*'3. Start web app and curl endpoints
List — all vaults
List — chain filtered
Snapshot
Reports
Timeseries
4. Verify no double-serialization
🤖 Generated with Claude Code