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 callawait 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.
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."regions": ["sin1"] in its
vercel.json, and every other project on the team is set to sin1 through the
API. Previously most listed three (for example platform used
['sin1', 'iad1', 'fra1']), which cost on two fronts:
- Provisioned memory. Fluid bills memory per instance, and each region keeps its own warm pool. Three regions means up to three pools held warm for the same request volume — close to pure waste for a low-traffic satellite.
- Latency. A request served from
iad1orfra1still reaches a Singapore database, so the extra regions were slower and billed.
exocorpse, richfield, kendra,
and yashie. These are client sites with their own audiences and their own
backends, so the sin1 argument — which rests on this platform’s database
being in ap-southeast-1 — does not apply to them. Everything else on the team
is single-region sin1, including projects that were previously single-region on
iad1.
Moving an already-single-region project from iad1 to sin1 does raise its
origin-transfer rate (0.27/GB), so it is not a saving on its own; it is
chosen for latency and for having one predictable region across the fleet.
Consolidation is where the money is; relocation is a consistency call.
Instance Size
functionDefaultMemoryType is a project setting with no vercel.json
equivalent, so it is set through the API and recorded here rather than in the
repo:
performance provisions a larger instance, so it bills more memory-hours for
the same wall time. Every project on the team now runs standard. A URL
shortener sitting on a performance instance was the clearest tell that the tier
had been set by habit rather than measurement.
Treat standard as the default and performance as something a project has to
earn with a measured number — an observed out-of-memory or a profiled CPU
ceiling — not by analogy with another app that looks busy.
Instance size is independent of region count, and the two were decided
separately: external client sites were moved to standard while keeping their
three regions, because their audiences are not Singapore-centric the way the
internal dashboards are.
Both settings take effect on the next deployment, and both are reversible
without a code change.
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
- Prefer a persisted watermark to a fixed lookback. A job that scans from
lastCheckedAtis cadence-independent: slowing it delays the result but never drops work.docker-recovery-alertsis the reference implementation, which is why it safely runs every 5 minutes instead of every minute. - Justify sub-5-minute schedules.
* * * * *is 43,200 invocations per month per job. Onlysample-resourcesearns 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. - 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 optimization —
minimumCacheTTLis 7 days increateTuturuuuNextConfig, not the 60-second default. - Client polling — shared hooks (
use-notifications,use-active-timer-session,use-calendar-sync) all setrefetchIntervalInBackground: false, so hidden tabs stop polling. - Canceled Git deployments —
vercel.jsonsetsgit.deploymentEnabled: false, so pushes create a deployment record that is canceled with 0 build seconds. The long list ofCANCELEDdeployments in the dashboard is cosmetic, not billed. - Preview deployments — the
vercel-preview-*.yamlworkflows areworkflow_dispatchonly and gated onTRUSTED_PREVIEW_DEPLOY_ACTORS. - Builds —
vercel buildruns on GitHub Actions runners, so build compute is not billed as Vercel build minutes. - Web Analytics —
@vercel/analyticsis not mounted in any app, so the per-event meter stays near zero even though the dashboard toggle is on.