Skip to main content
Tuturuuu runs ~30 monorepo apps plus client projects as separate Vercel projects on a single Pro team. This runbook records how that spend is shaped, so future changes are made against measured behavior instead of intuition.

The Shape Of The Bill

The important thing to internalize: traffic volume is not the problem. Edge requests and data transfer sit far inside the included allowance and cost nothing. Essentially the entire bill is Fast Origin Transfer and Fluid Active CPU, both of which are billed from zero with no free tier. The consequence is that spend tracks how bytes are produced, not how many requests arrive. Adding traffic to a cached route is close to free. Adding one uncached dynamic route, or one always-on cron, is not.

Why Origin Transfer Dominates

Fast Origin Transfer (4 GB) is ~80% of Fast Data Transfer (5 GB). Origin transfer is only charged when a response travels from a function to the CDN, so that ratio says roughly four out of five bytes we serve bypass the CDN and are generated fresh by a function. That is expected for the authenticated dashboard surface — 343 files across the apps call await connection(), which is the correct way to opt an authed page into request-time rendering under cacheComponents. It is not expected for public surfaces. Any public, unauthenticated route served without Vercel-CDN-Cache-Control pays origin transfer on every single hit. apps/web/next.config.ts already demonstrates the pattern for the auth shell:
Cache-Control: max-age=0 keeps the browser honest while Vercel-CDN-Cache-Control lets the CDN serve the shared copy. Extending this to public marketing pages, the public form-filling surface at /f/<shareCode>, and storefront pages is the single largest available reduction in origin transfer.
Never add shared-CDN cache headers to a response whose body varies by user, workspace, or session. Vercel-CDN-Cache-Control caches at a shared edge, so a per-user body cached once is served to everyone.

Region Pricing Is Not Uniform

Function region changes the unit price substantially: sin1 charges 4.5x the iad1 rate for origin transfer.
Do not “fix” this by moving functions to iad1. Supabase runs in ap-southeast-1 and SES_DEFAULT_REGION is ap-southeast-1, so sin1 is the region that keeps functions next to their data. Moving compute to North America would add a cross-Pacific round trip to every database call. sin1 is the correct region; the lever is sending fewer bytes from it, not relocating it.
The reviewable question is region count, not region choice. Projects currently list three function regions (for example platform uses ['sin1', 'iad1', 'fra1']). Requests routed to iad1 or fra1 still reach a Singapore database, so the extra regions add cross-ocean latency while spreading deployments wider. Single-region sin1 is usually the better default here.

Always-On Crons

Cron invocations run forever, whether or not anyone is looking at the result. Current scheduled load: infrastructure is the majority of all scheduled work on the account because it samples at one-minute resolution.

Rules For Adding A Cron

  1. Prefer a persisted watermark to a fixed lookback. A job that scans from lastCheckedAt is cadence-independent: slowing it delays the result but never drops work. docker-recovery-alerts is the reference implementation, which is why it safely runs every 5 minutes instead of every minute.
  2. Justify sub-5-minute schedules. * * * * * is 43,200 invocations per month per job. Only sample-resources earns it, because the monitoring dashboard renders a one-minute series and flags data stale after five minutes (RESOURCE_SAMPLE_MIN_INTERVAL_MS). Changing that cadence means changing that constant too, and accepting coarser graphs.
  3. Ask whether it should be on-demand. A dashboard that is viewed a few minutes a week does not need a 24/7 sampler. Sampling on page view, or behind a feature flag, removes the cost floor entirely.

What Is Already Optimized

Verified, so nobody re-investigates these:
  • Image optimizationminimumCacheTTL is 7 days in createTuturuuuNextConfig, not the 60-second default.
  • Client polling — shared hooks (use-notifications, use-active-timer-session, use-calendar-sync) all set refetchIntervalInBackground: false, so hidden tabs stop polling.
  • Canceled Git deploymentsvercel.json sets git.deploymentEnabled: false, so pushes create a deployment record that is canceled with 0 build seconds. The long list of CANCELED deployments in the dashboard is cosmetic, not billed.
  • Preview deployments — the vercel-preview-*.yaml workflows are workflow_dispatch only and gated on TRUSTED_PREVIEW_DEPLOY_ACTORS.
  • Buildsvercel build runs on GitHub Actions runners, so build compute is not billed as Vercel build minutes.
  • Web Analytics@vercel/analytics is not mounted in any app, so the per-event meter stays near zero even though the dashboard toggle is on.

Checking Current Spend

The dashboard breakdown lives under Usage, switchable between By Project and By Product. Read it By Product first: that tells you which meter is moving. Only then switch to By Project to find the owner. Reading it the other way around leads to optimizing a project that is merely large rather than a meter that is actually expensive.