One day after a client site went live, I woke up to the kind of alert that ruins breakfast: almost every content page refused to load. All you got was Cloudflare's 522 screen, "Connection timed out", after the browser hung for about 90 seconds. But one detail stood out immediately. The homepage was perfectly healthy, fast and smooth. Only the content detail pages were falling over, one after another. And the sitemap, which should have listed around 1,800 URLs, was stuck at just 35.
That combination is what made me pause. If the server were truly down, the homepage should have gone down with it. Instead it served flawlessly while every dynamic page returned 522. Something selective was happening, and selective rarely means "the server is dead".
The dead ends I chased first
My first reflex aimed at the wrong target. Since the error was a Cloudflare screen, I blamed Cloudflare. Status page: clean. I flipped the orange proxy off so requests hit the origin directly, and they still hung 90 seconds before timing out. So the edge was not the problem, it was just the messenger.
Suspect two: the application host on Railway. The app was alive: no crash loop, memory fine, no restarts, no fatal exception in the logs. I redeployed once for good measure and got the same result.
At that point I asked the right question: why is the homepage a different story? The answer was the big clue. The homepage was cached at the Cloudflare edge, so it served without ever touching the origin. The detail pages were not cached, so every request forced a full server render, and that render was what hung. The problem lived in something the render was doing, not in the network.
The root cause
Every detail-page render pulls its data from Sanity. So I did the most basic thing I should have done from the start: I hit the Sanity API directly with curl, bypassing the whole application layer. The response cracked it open at once:
{"error":"plan_limit_reached","statusCode":402,"message":"API Requests quota limit reached"}HTTP 402, plan_limit_reached. The API quota was spent. A few minutes later I realized Sanity had also sent a 100 percent quota warning email that drowned in the overnight noise.
The numbers added up. Sanity's free tier grants roughly 250,000 API requests per month. The catch: staging and production shared one dataset, and each page render fired off a fair number of queries. Launch traffic, plus bots and crawlers, plus a still-active staging environment, drained a month's allowance in hours.
But an exhausted quota alone does not explain why the pages hung for a full 90 seconds instead of failing fast. This is where my own misconfiguration made it worse. My Sanity client was created with useCdn: false and, more damning, no timeout at all:
export const client = createClient({
projectId,
dataset,
apiVersion: "2024-01-01",
useCdn: false, // always the live API, hits the main quota bucket
// no timeout: fetches hang indefinitely
});When the API answered 402, the untimed fetch sat there waiting for a response that would never be useful, and with no ceiling it waited until Cloudflare itself gave up at 90 seconds and returned 522. So the 522 was only a symptom. The real illness was the 402 I never handled.
There was a subtler second-order effect too. One helper, getApiSettings(), reads configuration from Sanity. On a 402 it returned null, and a few external data fetchers that depended on that setting quietly fell back to placeholder data. So some data showed up wrong with no visible error at all.
The fix
The fix came in two layers, and the order matters. The first ends the incident; the second makes sure the symptom can never be this vicious again.
First layer, immediate: upgrade the Sanity tier to a paid plan (Growth) so the quota stops being a wall. That is a call that should have been made on day one for a production site. Once the quota had headroom, the detail pages rendered again and the 522 cleared within minutes.
Second layer, defensive, so an external failure never hangs for 90 seconds again: set an explicit timeout and turn on useCdn: true.
export const client = createClient({
projectId,
dataset,
apiVersion: "2024-01-01",
useCdn: true, // editorial content via the CDN, separate quota bucket
timeout: 5000, // milliseconds: fail fast, never hang
});These two lines change the character of failure. useCdn: true serves editorial content from Sanity's CDN, which has a separate quota bucket and shrugs off traffic far better, so pressure on the main API drops sharply. timeout: 5000 guarantees that even if something upstream breaks, the fetch fails within 5 seconds with a clear error instead of hanging until Cloudflare rules 522. Failing fast beats hanging long, every time.
Takeaways
- If the homepage is healthy but dynamic pages return
522, suspect the origin render, not the network. An edge-cached homepage hides a problem that only appears when a render touches the origin. - A Cloudflare
522is almost always a symptom, not a cause. Chase whatever is hanging behind the origin, which here was a402from Sanity. - Hit the external API directly with
curlearly. Onecurlsurfaced theplan_limit_reachedthat the522screen would never reveal. - For a production site, use a paid tier from day one, and never let staging share the same dataset and quota as production.
- Every fetch to an external service needs an explicit
timeout. An unbounded fetch turns an exhausted quota into a 90-second timeout. - For editorial content, turn on
useCdn: true. Separate quota bucket, and far more resilient to a launch-day traffic spike.
