Next.js 14部署Vercel时SSE报错:任务15秒后超时
我正在开发一款使用Server-Side Events(SSE)流式传输Twitch账号名至显示屏,以切换/展示Twitch嵌入组件的应用。本地运行正常,但部署到Vercel后出现错误:Task timed out after 15.01 seconds,且客户端无法重连,错误持续出现。
代码片段
Route.ts
export async function GET() { const encoder = new TextEncoder(); let screenId: string; const cookieStore = cookies(); const TokenCookie = cookieStore.get("Token"); if (!TokenCookie) { return new Response("Token not found", { status: 401 }); } try { const Token = await verify(TokenCookie.value); const TokenPayload = JSON.parse(Token.payload as string); screenId = TokenPayload.id; console.log("Token ID:", screenId); } catch (error) { console.error("Error verifying token:", error); return new Response("Invalid token", { status: 403 }); } const customReadable = new ReadableStream({ async start(controller) { redisSubscriber.subscribe(setKey, (err) => { if (err) { console.error("Error subscribing to Redis:", err); controller.error(err); // Signal an error in the stream return; } }); redisSubscriber.on("message", (channel, message) => { const parsed = JSON.parse(message); if (channel === setKey && parsed.screenId === screenId) { controller.enqueue(encoder.encode(`data: ${message}\n\n`)); } else { console.log("Invalid token, ignoring message."); } }); redisSubscriber.on("error", (error) => { console.error("Redis error:", error); controller.error(error); }); } }); return new Response(customReadable, { headers: { "Content-Type": "text/event-stream; charset=utf-8", "Connection": "keep-alive", "Cache-Control": "no-cache", "Content-Encoding": "none", }, }); }
page.tsx
useEffect(() => { let eventSource = new EventSource("/api/stream"); eventSource.addEventListener("message", (event) => { try { const streamData = JSON.parse(event.data); if (streamData.stream) { setTwitchChannel(streamData.stream); } if (streamData.events) { setEvents(streamData.events); } if (streamData.fullscreen && streamData.fullscreen === true) { setTimeout(() => { router.push(`/screen/fullscreen/?channel=${twitchChannel}`); }, 0); } } catch (error) { console.error("Error parsing JSON:", error); } }); eventSource.addEventListener("error", () => { eventSource.close(); setTimeout(() => { eventSource =new EventSource("/api/stream"); }, 1000); }); }, [router, twitchChannel, events]);
已尝试的方案
我尝试过设置runtime为edge/node/serverless、移除Redis等方法,也参考了大量SSE代码示例,但均无效。另外因体积过大无法使用Edge runtime。
解决思路与建议
核心原因:Vercel Serverless函数超时限制
Vercel Serverless函数默认最大执行时间是15秒,这就是你遇到Task timed out错误的直接原因。SSE需要维持长连接,而Serverless函数的设计是短请求处理,不适合长期持有连接,到时间就会被强制终止。调整SSE连接策略
在SSE流中定期发送心跳包(比如每10秒发送一次data: \n\n),虽然不能突破Vercel的最大执行时间限制(最高90秒,依计划而定),但能避免连接被提前判定为空闲断开。改用支持长连接的部署方式
放弃Serverless函数,改用Vercel的Container部署(自定义运行环境,支持长连接),或者选择其他原生支持SSE/WebSocket长连接的平台(如Fly.io、Railway)。修复Redis订阅资源泄漏
当前代码中Redis订阅在函数结束后不会被取消,会导致资源泄漏。需要在ReadableStream的cancel方法中添加取消订阅的逻辑:const customReadable = new ReadableStream({ async start(controller) { // 原订阅逻辑... }, async cancel() { redisSubscriber.unsubscribe(setKey); redisSubscriber.removeAllListeners("message"); redisSubscriber.removeAllListeners("error"); } });优化客户端重连逻辑
把固定1秒重试改成指数退避策略(比如1秒、2秒、4秒,最大到30秒),避免频繁请求触发Vercel的限流机制:let retryDelay = 1000; eventSource.addEventListener("error", () => { eventSource.close(); setTimeout(() => { eventSource = new EventSource("/api/stream"); retryDelay = Math.min(retryDelay * 2, 30000); }, retryDelay); });简化Token验证流程
确保Token验证逻辑尽可能快速完成,避免因这部分耗时过长提前触发函数超时。
内容的提问来源于stack exchange,提问作者Magnus Gjerstad

