DEV Community

Cover image for The Edge Runtime That Never Was: Fixing a Silent AI Timeout in a SvelteKit + Netlify App
Aribu js
Aribu js

Posted on

The Edge Runtime That Never Was: Fixing a Silent AI Timeout in a SvelteKit + Netlify App

Summer Bug Smash: Clear the Lineup πŸ›πŸ›Ή

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

SMM Turbo is a fast, DOM-based Instagram carousel editor built with Svelte 5
and a hybrid AI co-pilot: Gemma 4 31B for deep strategy generation, Llama 3.1
(via Groq) for fast micro-tasks. I first wrote about building it for the
Gemma 4 Challenge,
where I described how I solved a serverless timeout problem by moving the AI
endpoint to Netlify's Edge runtime.

Turns out, I hadn't.

Bug Fix or Performance Improvement

In my previous article, I wrote this, confidently:

"I bypassed heavy official SDKs and implemented a native fetch request within
an Edge Function (runtime: 'edge'). This extended the timeout limit to
30 seconds..."

And the code backed that claim up β€” right at the top of /api/ai/+server.ts:

// Switching to Edge runtime to bypass Netlify's 10-second limit
export const config = { runtime: 'edge' };
Enter fullscreen mode Exit fullscreen mode

It looked right. It read like a solution. It just didn't do anything.

export const config = { runtime: 'edge' } is Vercel-specific syntax. On
@sveltejs/adapter-netlify, Edge Functions aren't opted into per-route β€”
they're enabled once, for the whole app, in svelte.config.js:

adapter({ edge: true })
Enter fullscreen mode Exit fullscreen mode

My svelte.config.js only ever had adapter({ split: false }). edge was
never set. Per Netlify's own docs:
if edge isn't explicitly true, SSR β€” and every server endpoint β€” runs on
standard Node-based Netlify Functions, not the Deno-based edge runtime. My
"Edge Function" was a regular Netlify Function the entire time, running with a
real 10-second timeout, not 30.

Worth noting: this didn't throw any error or warning, because export const
config
is valid SvelteKit syntax for passing adapter-specific options β€”
@sveltejs/adapter-netlify simply doesn't read the runtime key. It's
silently unused, which is exactly what made it dangerous.

I confirmed this two independent ways before touching any code:

  1. Build logs only ever showed a "Functions bundling" step packaging sveltekit-render.mjs. No "Edge Functions bundling" step ever appeared β€” because nothing was ever built as an edge function.
  2. Sentry, once instrumented, tagged every captured event with Cloud Resource: AWS_Lambda_nodejs22.x β€” direct, independent confirmation this endpoint runs on a standard Lambda-backed Node function.

The false assumption had already shaped other code decisions. An earlier version
of the same file had this:

generationConfig: {
  temperature: 0.7,
  maxOutputTokens: 1024 // Reduced tokens to speed up response and fit into 30 sec
}
Enter fullscreen mode Exit fullscreen mode

maxOutputTokens had been deliberately trimmed down β€” sacrificing response
quality β€” to fit a 30-second budget that was never real. The actual budget had
been 10 seconds the whole time, and nothing was protecting against blowing past
it. When a Gemini response ran long, the connection just died β€” a silent,
unhandled 502.

Code

Fix commit: fcb90f6 β€” Add AbortController timeout, input validation, Sentry tracing

// BUG (found & fixed): the line below used to read
// `export const config = { runtime: 'edge' };` with a comment claiming
// "Edge runtime gives us 30 seconds instead of 10". That syntax is
// Vercel-specific and is silently ignored by @sveltejs/adapter-netlify.
// Netlify Edge Functions must be enabled via `edge: true` in the adapter
// options in svelte.config.js β€” which was never set here. As a result,
// this endpoint always ran as a standard Node-based Netlify Function
// with the real default limit of 10 seconds (26s on paid plans), not 30.
// Source: https://docs.netlify.com/build/frameworks/framework-setup-guides/sveltekit/
const AI_TIMEOUT_MS = 8_000; // leaves a safety margin under the real 10s limit

async function fetchWithTimeout(
  url: string,
  options: RequestInit,
  timeoutMs: number
): Promise<Response> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...options, signal: controller.signal });
  } catch (err: any) {
    if (err.name === 'AbortError') {
      throw new Error('AI_PROVIDER_TIMEOUT');
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}
Enter fullscreen mode Exit fullscreen mode

I also went back and corrected the record β€” both the dead runtime: 'edge'
line and an incorrect "Netlify Edge Functions" claim in the project's own README:

- **Backend / Auth:** Supabase, Netlify Edge Functions
+ **Backend / Auth:** Supabase, Netlify Functions (Node.js)
Enter fullscreen mode Exit fullscreen mode

My Improvements

Along with the timeout fix, I added:

  • Input validation β€” prompt, mode, and slideCount were previously used completely unchecked. A bad slideCount could reach the model unclamped; an empty prompt would still trigger a full API call.
  • Clean error responses β€” raw provider error text (Google API Error (500): {...}) used to leak straight to the client. Now the client gets a clear, safe message instead.
  • A distinguishable timeout error (AI_PROVIDER_TIMEOUT) β€” so a slow provider and a genuinely broken request produce different, traceable failure modes instead of one generic 500.

Bonus: What Also Broke Along the Way

Instrumenting Sentry itself wasn't entirely smooth, and it cost real debugging
time β€” worth documenting since it's a strong "did I use this tool" story too.
After running the Sentry wizard and deploying, the site returned a bare 404 with
a completely clean build log. Three separate issues turned out to be involved:

1. devDependencies gets stripped from the function bundle. The Sentry
wizard adds @sentry/sveltekit to devDependencies by default. Netlify strips
devDependencies before packaging a function for deployment β€” anything needed
at runtime has to be in dependencies.

// ❌ Before β€” gets stripped from the Netlify Function bundle
"devDependencies": { "@sentry/sveltekit": "^8.0.0" }

// βœ… After β€” survives Netlify's function bundling
"dependencies": { "@sentry/sveltekit": "^8.0.0" }
Enter fullscreen mode Exit fullscreen mode

2. The default bundler chokes on Sentry's dynamic imports. esbuild
(Netlify's default function bundler) couldn't correctly resolve Sentry's dynamic
imports β€” the function built without errors, but crashed silently at runtime.
Switching to nft (Node File Trace) fixed it:

# netlify.toml
[functions]
  node_bundler = "nft"
Enter fullscreen mode Exit fullscreen mode

3. A failed function falls back to a bare 404, with an empty function log.
When the render function fails to even start, Netlify's routing falls back to a
static index.html, doesn't find one, and serves its own generic 404 β€” the
request never reaches the function, so its logs stay empty. This is what made it
so confusing to debug: a clean build log and a 404 that looked like a routing
problem, not a crashed function.

Best Use of Sentry

I instrumented the endpoint with Sentry tracing β€” one span per request, with
nested spans per AI provider (ai.provider.google vs ai.provider.groq), so I
could compare latency between Gemini and Groq side by side.

Shortly after deploying the fix, Sentry (Distributed Tracing + Error Monitoring)
caught a real timeout live in production β€” not staged, not simulated.

Sentry trace waterfall showing an 8.01s request flagged as 83% slower than average

The trace shows exactly where the time went: http.server β†’ ai.generate β†’
ai.provider.google β†’ the actual fetch call to Gemini. Instead of hanging
silently past the real 10s Netlify limit, the timeout fired cleanly at 8s, and
the user got a proper error message instead of a dead connection.

Sentry Tags panel showing ai_error_type: timeout and Cloud Resource: AWS_Lambda_nodejs22.x

The custom ai_error_type tag comes straight from the fix's error handling β€”
it's what let me filter and confirm this was specifically a timeout, not a
generic provider failure. The Cloud Resource tag is Sentry independently
confirming what the build logs already told me: this is a standard Lambda/Node
function, not an edge runtime β€” the piece of evidence that closed the
investigation.

Sentry Stack Trace pointing to fetchWithTimeout, unminified via source maps

Source maps made the stack trace point straight to fetchWithTimeout in the
actual source, not a minified bundle β€” which made debugging (and writing this
article) a lot easier.


One line of dead configuration β€” syntax borrowed from a different platform,
sitting silently on top of a real 10-second limit β€” had quietly shaped several
downstream decisions: a trimmed token limit, no timeout handling, no visibility
into failures, and a confident public claim in my own previous article that
turned out to be wrong. The fix itself is small: an AbortController, some
input validation, and clear error messages. What made it findable β€” and
provable β€” was Sentry tracing catching the exact failure mode live in production.

Thanks for reading!


I'm Bohdan (Aribu js) β€” indie hacker and frontend developer building SMM Turbo
and writing about Svelte, AI integrations, and web performance at
shcho-i-yak.pp.ua. This is a submission for
DEV's Summer Bug Smash, powered by Sentry.

Top comments (0)