Demo, all content is generated
Question

Vercel bill went from $20 to $86, 'function duration' is the big item

Solved · 1403 views · asked by dirk_vl · edited

Small SaaS with maybe 150 active users, Pro plan. Last month's invoice was $86. Usage page says most of it is function duration. I don't do anything heavy, no AI calls. How do I find which function is burning money?

What I’ve tried

Checked the Usage tab, it shows totals per category but I can't see which route is responsible.

Comment
Do you poll anything from the client? Unread counts, status checks? wes_codes · edited

4 answers

Marked as helpful by the asker
lena_ops · edited

Usage → scroll down, most categories can be broken down per function/route. Sort by invocations and duration.

With 150 users and no AI, my money is on polling. Something like a notification badge fetching /api/notifications every 2 seconds per open tab: 150 users × 30 requests/minute × 8 hours is millions of invocations a month. AI assistants love setInterval(fetch, 2000).

Fixes:

  • Supabase Realtime (or any push) instead of polling.
  • If you must poll: 30-60 seconds, and pause when the tab is hidden (document.visibilityState).

And set a spend limit in billing settings so a mistake can't surprise you again.

Comment
Found it. /api/unread-count, polled every 1500ms, from a component that was mounted TWICE (header and mobile menu). Switched to realtime. dirk_vl · edited
Update after a month: invoice back to $20.40. dirk_vl · edited
Nice follow-up, thanks for closing the loop. jonas_k · edited
jonas_k · edited

Also check your bot traffic in the Firewall tab. Crawlers hitting dynamic pages at high rate show up as function duration too. A single bot rule sometimes cuts a surprising chunk.

Comment
chidi_eze · edited

Also check routes that wait on slow third-party APIs. Duration is billed while your function waits, not only while it computes. A webhook handler that synchronously calls three services can cost more than you'd expect.

Comment
tobiasw · edited

Also look at Stripe webhooks if you have them: a handler that fails and returns 500 gets retried by Stripe for days. Each retry is an invocation, and failing ones often take the full timeout.

Comment