Hi, I’m a little confused about how to implement server-sent events on Vercel. I’ve seen conflicting info as to whether the time limit of 10 seconds for a server request on free plans is applicable to backend endpoints with event-streams that send server-sent events. I am running a longer running task on trigger.dev and using an endpoint to subscribe to that task and send a server-sent event back to the frontend when the task finishes. It works perfectly fine in development, but when deployed to vercel, I just see logs for events that happen in the first few seconds, then don’t see any logs beyond that, and the completion event never gets sent to the front-end. I tried adding a heartbeat/keepalive to see if that helped but it didn’t. Is this because the 10-second time limit is enforced for server-sent event endpoints as well? Below is my code for the SSE endpoint: logging events show up in vercel for the task being queued and when it starts executing, but not when it is completed. I’m also getting a log for the first heartbeat interval, but not beyond that. Any help would be appreciated. Or if there is a way to detect when Vercel is going to end the connection and alert the frontend so it can re-establish it. I’m willing to upgrade to the paid plan, but want to make sure that this is the actual issue first. And still, the 60-second limit won’t be enough for my full task to execute.
Thanks for any help!
import { eventHandler, setHeader } from 'h3'
import { runs } from "@trigger.dev/sdk/v3"
export default eventHandler(async (event) => {
// Set headers for SSE
console.log('subscribing to run', event.context.params.id)
setHeader(event, 'Content-Type', 'text/event-stream')
setHeader(event, 'Cache-Control', 'no-cache')
setHeader(event, 'Connection', 'keep-alive')
const id = event.context.params.id
try {
// Create a write function to send SSE
const write = (data) => {
console.log('Attempting to write SSE data:', data);
try {
event.node.res.write(`data: ${JSON.stringify(data)}\n\n`);
console.log('Successfully wrote SSE data');
} catch (writeError) {
console.error('Error writing SSE data:', writeError);
}
}
// Setup heartbeat interval
const heartbeat = setInterval(() => {
try {
console.log('Sending heartbeat');
event.node.res.write(': keepalive\n\n');
} catch (error) {
console.error('Error sending heartbeat:', error);
}
}, 5000); // Reduced to 5 seconds for testing
console.log('Heartbeat interval established');
// Use the documented subscribeToRun method
console.log(`Starting subscription to run ${id}`);
for await (const data of runs.subscribeToRun(id, {
apiKey: process.env.TRIGGER_API_KEY
})) {
console.log('Received run data for ' + id, JSON.stringify(data));
write({
status: data.status,
output: data.output
})
// End connection if job is complete
if (data.status === 'COMPLETED' || data.status === 'FAILED') {
clearInterval(heartbeat); // Clear heartbeat interval
console.log(`Run ${id} finished with status: ${data.status}`);
event.node.res.end();
console.log('Connection ended successfully');
break;
}
}
// Clean up if client disconnects
event.node.req.on('close', () => {
clearInterval(heartbeat); // Clear heartbeat interval
console.log(`Client disconnected from run ${id}`);
});
} catch (error) {
console.error('Error in job stream handler:', error);
try {
event.node.res.end(`data: ${JSON.stringify({ error: error.message })}\n\n`);
console.log('Sent error to client');
} catch (sendError) {
console.error('Error sending error to client:', sendError);
}
}
})