perf: run functions in Dublin and stop re-querying tenant data per render - #5
Merged
Merged
Conversation
…nder Production functions ran in iad1 (Washington) while Supabase is in eu-west-1 (Ireland), so every query crossed the Atlantic. Measured warm TTFB on tenant pages was 7.2-9.3s against 21-29 queries per request — roughly 300ms per query, because DATABASE_URL uses connection_limit=1 and the queries therefore run one at a time. - Pin functions to dub1 (same region as the database). - Memoize tenant context, branding and published site content per render with React cache(): the layout, generateMetadata, the public shell and the page each resolved them separately (3-5 duplicate queries/render). - Cache published branding, published site content (60s) and the marketer leaderboard (5min) across requests, keyed and tagged by companyId. The branding/site-content publish routes already revalidate that tag. Measured prod-equivalent queries per request (local trace): tenant / 29 -> 10 tenant /about 20 -> 5 tenant /properties 26 -> 7 tenant /contact 21 -> 6 The admin draft branding read stays uncached across requests: saving a draft does not revalidate the tag, so a shared cache would show operators stale drafts. Tenant isolation: cache keys and tags include companyId, and React cache() is request-scoped. The cached leaderboard is computed with a context carrying only companyId — findManyForTenant skips the company filter for super admins, so passing a viewer context through could have cached one tenant's cross-company aggregate and served it to all of that tenant's visitors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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.
Problem
Production functions ran in iad1 (Washington) while Supabase is in eu-west-1 (Ireland), so every query crossed the Atlantic.
x-vercel-idon every production response readcpt1::iad1::….Measured warm TTFB on tenant pages was 7.2–9.3s, against 21–29 queries per request — roughly 300ms per query. A US↔Ireland round trip is ~75ms; the rest comes from queries running one at a time, because
DATABASE_URLusesconnection_limit=1, which serializes everyPromise.allin the app.//properties/contact/aboutestateos.tech/Client-side delivery was never the problem: all JS/CSS/fonts/images finish within ~1.3s, while the HTML document alone took ~9.7s.
Changes
dub1(vercel.json) — same region as the database.cache(): the layout,generateMetadata, the public shell and the page each resolved tenant context, branding and published site content separately (the company lookup ran 4x per render, branding 3x, site content 2x).companyId. Both publish routes already callrevalidateTag('tenant-presentation:<companyId>').Measured prod-equivalent queries per request (local Prisma trace, dev-only queries excluded):
//about/properties/contactMeasured result (preview in dub1 vs production in iad1, same database)
/platform/api/readyzTenant pages could not be measured on the preview: tenant resolution is host-based and a preview URL cannot claim a tenant's custom domain. They are expected to improve far more than the platform page (29→10 queries plus the region change), to be confirmed against production after merge.
Tenant isolation
Cache keys and tags all include
companyId, and Reactcache()is request-scoped.The cached leaderboard is computed with a context carrying only
companyId— never the viewer's.findManyForTenantskips the company filter entirely whenisSuperAdminis true, so a super admin browsing a tenant's public site previously got a leaderboard aggregated across all companies; caching the viewer's result would have stored that under the tenant's key and served it to every visitor. This also fixes that pre-existing leak.The admin draft branding read stays uncached across requests: saving a draft does not revalidate the tag, so a shared cache would show operators stale drafts.
Notes
unstable_cacherather than"use cache": the latter requires Cache Components enabled app-wide, which changes how every dynamic page renders. The bundled Next 16 docs nameunstable_cacheas the supported path without it, anddashboard-shell.tsxalready uses this pattern.headers()/cookies(), so they are dynamic by construction. Data-layer caching is the available lever.DATABASE_URLusesconnection_limit=1. Raising it to5should compound with this change by letting parallel queries actually run in parallel.site-content-service.tsmigration-drift handling (theunavailablefallback), as it lives in the same file.Verified:
npm run checkgreen (535 tests, typecheck, lint, build). Preview deployment healthy, no cache-related runtime errors.🤖 Generated with Claude Code