Vercel Cron Job结合Nodemailer在Next.js中出现504错误求助
解决方案:Vercel 上 Next.js + Nodemailer 504 超时问题
你的判断没错,Vercel 免费计划下 Serverless Functions 确实有10秒超时限制,当查询任务+批量发邮件的流程超过这个时间就会触发504错误。以下是几个可行的解决思路,按实现成本从低到高排序:
1. 优化现有流程压缩执行时间
先从最基础的优化入手,尽量把执行时间控制在10秒内:
- 优化数据库查询:给任务表的「提醒日期」字段加索引,查询时只过滤当日需提醒的任务,避免全表扫描。比如用 Prisma 的话:
查询代码只取当日数据:model Task { id Int @id @default(autoincrement()) title String userEmail String remindAt DateTime @@index([remindAt]) // 加索引 }const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setHours(23, 59, 59, 999); const tasks = await prisma.task.findMany({ where: { remindAt: { gte: todayStart, lte: todayEnd } } }); - 批量复用 SMTP 连接:Nodemailer 的 transporter 会自动复用连接,不要每次发邮件都重新创建 transporter,把它提出来作为全局变量:
// 全局初始化,不要放在接口函数里 const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, }); - 简化邮件内容:避免复杂的 HTML 渲染逻辑,用纯文本或提前预编译好的模板,减少运行时处理时间。
2. 改用 Edge Functions(免费版支持60秒超时)
Vercel Edge Functions 的超时限制是60秒,比 Serverless Functions 宽松很多,只需要在接口文件顶部加一行配置即可切换:
// pages/api/setupReminder.js export const runtime = 'edge'; // 关键配置,切换为 Edge Function import nodemailer from 'nodemailer'; import { getTodayTasks } from '../../lib/db'; // 全局初始化 transporter const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, }); export async function POST(request) { try { const tasks = await getTodayTasks(); if (!tasks.length) return new Response('无待提醒任务', { status: 200 }); // 批量发送邮件,用 Promise.all 并行处理 const mailPromises = tasks.map(task => transporter.sendMail({ from: process.env.EMAIL_USER, to: task.userEmail, subject: `任务提醒:${task.title}`, text: `你有一个今日任务需要处理:${task.title}\n详情:${task.description}`, }) ); await Promise.all(mailPromises); return new Response('提醒邮件发送成功', { status: 200 }); } catch (error) { console.error('发送邮件失败:', error); return new Response('发送邮件失败', { status: 500 }); } }
3. 异步队列拆分任务(适合大量邮件场景)
如果邮件数量太多,60秒仍不够用,就把任务拆分:Cron 触发的接口只负责把待发送任务存入队列,然后立即返回;另一个接口负责从队列中分批取任务发送。
- 存入队列(用 Vercel KV,免费版有额度):
// pages/api/setupReminder.js export const runtime = 'edge'; import { kv } from '@vercel/kv'; import { getTodayTasks } from '../../lib/db'; export async function POST(request) { try { const tasks = await getTodayTasks(); if (!tasks.length) return new Response('无待提醒任务', { status: 200 }); // 把任务序列化后存入 KV 列表 await kv.lpush('reminder-queue', ...tasks.map(JSON.stringify)); return new Response('任务已加入发送队列', { status: 200 }); } catch (error) { console.error('加入队列失败:', error); return new Response('加入队列失败', { status: 500 }); } } - 处理队列(单独接口,用 Cron 每隔5分钟触发一次):
// pages/api/processQueue.js export const runtime = 'edge'; import nodemailer from 'nodemailer'; import { kv } from '@vercel/kv'; const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, }); export async function POST(request) { try { // 每次取10条任务处理,避免超时 const taskStrs = await kv.rpop('reminder-queue', 10); if (!taskStrs.length) return new Response('队列为空', { status: 200 }); const mailPromises = taskStrs.map(str => { const task = JSON.parse(str); return transporter.sendMail({ from: process.env.EMAIL_USER, to: task.userEmail, subject: `任务提醒:${task.title}`, text: `你有一个今日任务需要处理:${task.title}\n详情:${task.description}`, }); }); await Promise.all(mailPromises); return new Response('已处理10条队列任务', { status: 200 }); } catch (error) { console.error('处理队列失败:', error); return new Response('处理队列失败', { status: 500 }); } }
4. 改用 Vercel Email 官方服务(推荐)
Vercel 官方提供的 Email 服务集成更顺畅,发送性能优于自行配置 Nodemailer,且支持 Edge Functions:
- 安装依赖:
npm install @vercel/email - 创建邮件模板(比如
app/emails/reminder.html):<!DOCTYPE html> <html> <body> <h1>任务提醒:{{title}}</h1> <p>你有一个今日任务需要处理:{{description}}</p> </body> </html> - 修改接口代码:
// pages/api/setupReminder.js export const runtime = 'edge'; import { render } from '@vercel/email'; import { sendEmail } from '@vercel/email/server'; import { getTodayTasks } from '../../lib/db'; export async function POST(request) { try { const tasks = await getTodayTasks(); if (!tasks.length) return new Response('无待提醒任务', { status: 200 }); const mailPromises = tasks.map(task => sendEmail({ from: process.env.EMAIL_FROM, to: task.userEmail, subject: `任务提醒:${task.title}`, html: render('emails/reminder.html', { title: task.title, description: task.description }), }) ); await Promise.all(mailPromises); return new Response('提醒邮件发送成功', { status: 200 }); } catch (error) { console.error('发送邮件失败:', error); return new Response('发送邮件失败', { status: 500 }); } } - 在 Vercel 控制台配置 Email 服务,绑定自定义域名或使用 Vercel 默认域名。
内容的提问来源于stack exchange,提问作者EHTASHAM TOOR
相关产品推荐
相关产品推荐

