You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

  1. 安装依赖:npm install @vercel/email
  2. 创建邮件模板(比如 app/emails/reminder.html):
    <!DOCTYPE html>
    <html>
    <body>
      <h1>任务提醒:{{title}}</h1>
      <p>你有一个今日任务需要处理:{{description}}</p>
    </body>
    </html>
    
  3. 修改接口代码:
    // 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 });
      }
    }
    
  4. 在 Vercel 控制台配置 Email 服务,绑定自定义域名或使用 Vercel 默认域名。

内容的提问来源于stack exchange,提问作者EHTASHAM TOOR

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 19:59:56