How I left Vercel, Part 1: The Bill
I was building a cost dashboard when I found a four-figure charge for functions that did thirty cents of actual work. Then Vercel disclosed a breach, and the same architectural pattern showed up in both stories.
Originally published on joshduffy.dev. Republished here as part of the FMLOps corpus.
Why now
I wrote this three weeks ago and sat on it. A billing dispute is a satisfying personal story but a petty blog post, and I wasn't sure the world needed another "I left Platform X" narrative from a guy running thirteen side projects (which is either impressive or a cry for help depending on your perspective).
Then yesterday Vercel disclosed a security breach.
Customer environment variables stored on their platform had been exposed. API keys. Database credentials. Signing secrets. The ones that leaked were variables customers had not explicitly marked as "sensitive," which in Vercel's architecture means: not encrypted at rest. Readable with internal access. The attack chain went like this: a third-party AI tool called Context.ai was compromised, which gave an attacker access to a Vercel employee's Google Workspace, which gave them access to Vercel's internal systems, which gave them access to every customer secret that hadn't been proactively flagged for special treatment. A threat actor is now reportedly selling this data for two million dollars on a hacker forum.
I want to be precise about my intentions here. A breach like this is genuinely terrible for everyone involved. I know because I spent my weekend rotating keys. I left Vercel weeks ago, my variables were marked sensitive, and I had no particular reason to believe I was affected. But the disclosure was light on details when it first surfaced a few days ago, and acting responsibly meant assuming the worst and rotating everything anyway. That was Saturday.
As I type this sentence, at 10:02 PM on Monday, I just received Vercel's email notification about the breach. It says: "At this time, we do not have reason to believe that your Vercel credentials or personal data have been compromised." Then, three lines later, it recommends that I "review and rotate environment variables" and "take advantage of the sensitive environment variables feature."
Take advantage of the sensitive environment variables feature. In a breach notification. For a breach caused by that feature being opt-in.
I am not publishing this to say I told you so. I am publishing this because I kept reading the disclosure and seeing the same machine.
In my story: Vercel's serverless functions default to wall-clock billing. The model that charges you for a function sitting idle on a dead socket is the default. The model that charges for actual CPU work exists, but you have to know it's there and enable it.
In yesterday's story: Vercel's environment variables default to unencrypted. The model that leaves your API keys readable at rest is the default. The model that encrypts them exists, but you have to know it's there and mark each variable yourself.
When my bill hit $1,243, Vercel support linked me to their Shared Responsibility Model documentation. When credentials leaked, Vercel told customers to take advantage of the feature that would have prevented the breach if it had been the default. Same playbook. The platform creates the dangerous default, the dangerous default does what dangerous defaults do, and then the platform tells you which setting you should have changed.
My misconfigured connection pool became a four-figure bill because no circuit breaker existed. Eight days. No intervention. A single compromised employee account became a mass credential exposure because environment variables weren't encrypted unless customers asked. Same architectural decision, different failure mode: do not protect the customer unless they explicitly request protection.
(Vercel's billing dashboard also detected my 122x spending anomaly and displayed it right next to an upsell for their observability product. They've since rolled some of those features into paid plans, so I'll give them partial credit for closing that particular gap. But the instinct is the same. Detect the problem. Make the customer pay extra to hear about it. Or in the case of encryption: make the customer opt in to being protected from your own infrastructure.)
At the end of this post I wrote that the pattern "stops looking like a series of independent product decisions and starts looking like architecture." I wrote that about billing. The same architecture just leaked customer secrets.
I should say two things honestly. First: I am not pretending that leaving Vercel was painless. The migration to Cloudflare took weeks of actual work, involved at least three distinct categories of suffering, and there was one night around 11pm where I genuinely considered paying the $1,243 and staying put forever. This post tells the billing story and the decision to leave. The part where leaving actually happens is in Part 3, and it does not pretend to be fun. Second: there are other parts of this journey that were painful in different ways. I am starting with the billing dispute because yesterday's news made its particular lessons impossible to ignore.
Here is what happened.
01
The bill
The hotel lobby had one bar of Wi-Fi. I was in the Dominican Republic with my family, building a cost dashboard because my Vercel bill kept changing month to month and I could not figure out why. I run thirteen projects, which is either impressive or a cry for help depending on your perspective. Vercel was supposed to be a line item. Twenty bucks, maybe seventy-five on a busy month. My previous invoices: $0, $20, $21.60, $43.49, $75.17. Total lifetime spend before this cycle: $160.26. I pulled the data and found this instead.
One billing period exceeded all of them combined. I had that specific feeling you get when you open an envelope from the IRS and it is not a refund.
I pulled the data. Here is what Vercel's own dashboard showed.
| Metric | Value |
|---|---|
| Function Duration (billed) | $1,237.45 |
| Fluid Active CPU (actual compute) | $0.30 |
| Ratio: billed to actual CPU | 4,125 : 1 |
| Timeout rate | 73.7% |
| Timeout invocations / total | 800,883 / 1,086,040 |
| Previous cycle on-demand usage | $29.60 |
| 3-month average (on-demand) | $10 |
I need you to sit with that ratio for a moment. $1,237.45 billed. $0.30 in actual CPU work. Thirty cents. Not thirty dollars. Thirty cents. The platform charged four thousand times more for idle waiting than for actual computation. I would like to emphasize that these are Vercel's numbers, from Vercel's dashboard, which I screenshotted before they could change.
The cause turned out to be embarrassingly simple. My PostgreSQL connection pool was misconfigured. The pool had no connection timeout (it defaults to "wait forever," which is exactly as safe as it sounds), and Supabase's connection limit got exhausted within hours. After that, every new serverless function instance asked for a database connection, received silence, and sat there. For sixty seconds. Then Vercel killed it with a timeout error and spun up another one to do the same thing.
This was my mistake. I am not disputing that. But what happened next is the point.
This ran for
02
How I got here
I created my Vercel account in November 2025. I was running Next.js and Astro projects, deploying fast, and Vercel built Next.js, so it was the obvious choice. Push to deploy. Instant previews. It just worked. The account existed for five months before the $1,243 bill. Here is how those five months went.
On November 29th, my deploys started failing. "your@workemail attempted to deploy a commit to [my] projects on Vercel through the Vercel CLI, but they are not a member of the team." Every failure email had the same primary call to action: a large button that said
I upgraded. Receipt for $20 at 6:57 PM that evening. I later realized the team setup was wrong, removed the extra seat, and got a $20 refund on December 24th. But I stayed on Pro.
Here is what the upgrade changed that I did not know about at the time: on the Hobby plan, the maximum function duration is 10 seconds. On Pro, it defaults to 300 seconds. My functions were configured to 60 seconds, which is how each one accumulated a full minute of billing before dying. But the Pro plan
There is also a setting called "Fluid Compute" that bills for CPU time instead of wall-clock time. It was disabled on my account. I did not know it existed. A function hanging on a dead socket uses zero CPU. Under Fluid Compute, it would cost zero. Under the
03
The response
I filed a support case. Case #1061793. What followed was a masterclass in saying "no" while sounding sympathetic. I have the complete thread. Every quote below is verbatim.
The L1 engineer
Callum, Customer Support Engineer, responded within ten hours. His position was clear from the first paragraph.
Callum, Customer Support Engineer, Vercel
This is technically accurate in the same way that "the Titanic completed most of its voyage" is technically accurate. The
The anomaly he saw and ignored
Callum saw the anomaly. He wrote it down.
Callum
Read that again. He identified a 4x anomaly on day 2. He wrote it down in his response to me. The system he works for did nothing with that information for
The notification timeline
| Date | Event | Running total |
|---|---|---|
| Mar 17 | Billing cycle starts | $0 |
| Mar 19, 1:58 PM | Budget alert: 75% | ~$151 |
| Mar 19, 11:13 PM | Budget alert: 100% | ~$200 |
| Mar 20 to 24 | Five days of silence | $200 to ~$810 |
| Mar 25, 12:43 AM | Next notification | ~$810 |
| Mar 25, 12:29 PM | Pause triggered | ~$901 |
Between the $200 alert and the next notification, the bill quadrupled. Vercel's own Spend Management page shows my previous cycle was $29.60 and my 3-month average was $10. The current cycle showed $1,223.15 at the time of the screenshot (the final invoice came to $1,243.15). A 122x spike over the average. Their own UI displays both numbers side by side. Adjacent to this information, the dashboard shows an upsell: "Get alerted for anomalies. Upgrade to Observability Plus."
They can detect the anomaly. They charge extra to tell you about it.
The billing escalation
When the case reached Jordan, a billing escalation engineer, the offer was immediate and final.
Jordan, Staff Customer Support Engineer, Vercel
$250 back on a $1,243 bill. For an account where 74% of function invocations were timeout failures documented in their own dashboard. Jordan wanted me to know this was generous.
"The amount will not increase." That sentence ended the conversation. Not because I accepted the offer, but because I realized I was negotiating with a billing system, not a person. The system had decided. The people were explaining the system's decision.
So I decided too. Full exit. Not just the expensive project. Everything. Every project, every domain, every function. Delete the account entirely.
And then I had to raise my spend cap to $1,500 to keep everything running while I migrated. Because the alternative was Vercel killing all my production sites. You cannot leave quickly when the platform holds your domains hostage. You have to keep paying while you pack.
While all of this was happening, someone from Vercel's engineering team visited my LinkedIn. I noticed because LinkedIn tells you when people look at your profile, and I was not getting a lot of traffic from Swedish developers that week. I do not know what he was looking for. Maybe he was curious about the person behind the support ticket. Maybe he was checking whether I was the kind of person who writes blog posts. If so: hi.
04
One more thing
While I was gathering evidence for this post, I checked the team members page. One member: me. Then I checked the billing page. "Additional Team Seats: 1. $20.00."
One member. Billed for two. Twenty dollars a month for a seat that no one occupies. I removed the extra member in December 2025 and got a refund. The charge came back. It has been on every invoice since.
I understand that this is probably a bug and not a deliberate decision by a person at Vercel to charge me for a ghost. I understand that twenty dollars a month is not a lot of money. I understand that I could have caught this earlier if I had audited my invoices more carefully, which, ironically, is exactly what I was doing when I found the $1,243 bill.
But I do think that if you are going to build a billing system that charges for phantom team members on an account with one person, and also charges 4,125 times more for idle time than for compute, and also paywalls anomaly detection behind a separate subscription, and also defaults to the billing model that generates the most revenue rather than the one that reflects actual usage, then at some point the pattern stops looking like a series of independent product decisions and starts looking like