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.
- Live project: https://smm.shcho-i-yak.pp.ua
- Public repo: https://github.com/bodikinf/smm-turbo-open
- Fix commit: fcb90f6 β Add AbortController timeout, input validation, Sentry tracing
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' };
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 })
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 is valid SvelteKit syntax for passing adapter-specific options β
config
@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:
-
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. -
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
}
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);
}
}
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)
My Improvements
Along with the timeout fix, I added:
-
Input validation β
prompt,mode, andslideCountwere previously used completely unchecked. A badslideCountcould reach the model unclamped; an emptypromptwould 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" }
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"
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.
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.
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.
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)