TL;DR: Passed a raw JS
Dateobject to postgres.js (my Postgres client) as a query param. The driver threwERR_INVALID_ARG_TYPE. The rate limiter that made that call was the firstawaitin my route — so the whole endpoint was dead. Serializing the date fixed it in one line.
The Setup
An AI-assisted chat tool I built for a cancer education business helps cancer patients find oncology specialists.
Real people. Real stakes. Live in production.
The Wall
On June 2nd I opened Arc (my browser, driven via Chrome DevTools Protocol — more on that in a sec) and found every single /api/chat request returning 500.
The entire chat was down.
My first instinct — honestly — was the LLM integration is broken. That's where your brain goes, right? It's an AI chat, so obviously the AI part is the culprit.
Wrong.
What I Actually Found
I pulled Vercel runtime logs and the error jumped out immediately:
ERR_INVALID_ARG_TYPE
Not a model timeout. Not an embedding failure. Not even a network blip.
I traced it back to checkRateLimit — the function that checks whether a user has exceeded their chat limit before the actual request ever goes to the LLM.
Inside that function, I was passing a raw JS Date object directly as a query param to postgres.js.
postgres.js didn't know what to do with an object. It couldn't bind it. So it threw.
And that was it. Game over.
Why It Killed EVERYTHING
Here's the part that makes this more than a "remember to serialize your dates" story.
checkRateLimit was the very first await in the route handler.
The function that was supposed to protect the route became the thing that killed it — every single time, for every single user, no exceptions.
Your defensive code can become a single point of failure. That lesson hit me harder than the bug itself.
The Fix
Stopped passing the Date object raw. Handed postgres.js a serialized value it could actually bind.
One line. Route came back up. Verified end-to-end in Arc, confirmed 500 → 200 flipping live.
Total downtime: caught same day.
Why This Matters to Me
I build a lot of AI products that touch real people — cancer patients, in this case — and the thing that goes wrong is almost never the thing you'd guess. The LLM was fine. The embeddings were fine. The rate limiter — four lines of boring guard-clause code — was the bomb.
The boring code is the blast radius.
If you've got a guard running before your main logic: test it in isolation, make sure it fails gracefully, and double-check anything you're handing to a database driver. An object that "looks like data" to JavaScript is not data to the driver.
Date objects especially. They'll get you every time.
P.S. The cleanup after this fix had its own near-disaster — but that's a story for another post.