PAYATHON 2026

NextAuth 的 linkAccount() 为何只在 Vercel 上卡住?

数据阿明

目前能确定什么?

现有材料还不能确定根因,也不足以认定这是 @prisma/adapter-pg 的已知缺陷,或 pg Pool 与 Vercel cold starts 不兼容。下一步应定位 prisma.account.create() 究竟停在获取连接、驱动执行还是数据库处理阶段,再决定修复方式。

同一生产数据库可以被本地应用写入,Vercel 的普通 API routes 也能写入 Account 表,但 OAuth callback 中的绑定操作仍会卡住。因此,应优先排查 callback 的调用顺序、连接占用情况和实际运行环境。

Prisma Accelerate 和 PgBouncer 可以列为后续方案,但现有证据还不足以说明必须引入它们。

如何理解现有日志?

原示例直接调用了:

const createPromise = prisma.account.create({
  data: account
});

随后通过 Promise.race() 与一个 10 秒计时器竞争。这个包装只能说明调用没有在期限内返回,不能说明等待发生在哪一层。

解读现有日志时,需要注意:

  • TCP 连接成功不能排除所有连接问题。已有连接正常,不代表后续连接申请、认证或数据传输都正常。
  • Account INSERT 未出现在日志中是重要线索,但需要确认日志覆盖范围。应先确认查看的是实际目标实例,并且日志配置能够记录该语句。
  • events.linkAccount 未触发与绑定流程未完成一致,无法区分连接池等待和查询执行等待,也就不能进一步缩小范围。
  • $executeRaw 仍然经过 Prisma 和所配置的 driver adapter。它同样卡住,可以降低模型字段处理异常的排查优先级,但它没有绕过整条数据库访问链。
  • 登录成功不一定证明 Account 创建正常,需要区分是否进入了新的账号绑定流程。配置使用 session: { strategy: "jwt" },也不能仅凭会话可用就推断数据库 Session 表写入成功。

原日志中的 timeout after 5s 与代码中的 10000、实际耗时 10006ms 也不一致。应先确认这些记录来自同一个 deployment 和同一次请求,避免用不同部署的日志推断故障。

第一步:观察实际使用的连接池

把传给 PrismaPg 的 Pool 提取出来,供诊断代码读取状态。不要另外创建一个 Pool 来观察,否则看到的可能不是发生等待的那个实例。

以下示例沿用问题中已有的 PrismaPg(new Pool(...)) 构造方式,前提是当前依赖组合支持该接口。示例中的 5 秒连接超时用于诊断,并非已验证的修复参数。

import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";

const globalForPrisma = globalThis as unknown as {
  prisma?: PrismaClient;
  pgPool?: Pool;
};

export const pgPool =
  globalForPrisma.pgPool ??
  new Pool({
    connectionString: process.env.DATABASE_URL,
    connectionTimeoutMillis: 5000,
  });

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    adapter: new PrismaPg(pgPool),
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.pgPool = pgPool;
  globalForPrisma.prisma = prisma;
}

在 linkAccount() 开始、执行期间及失败时,记录:

console.info("[linkAccount] pool", {
  total: pgPool.totalCount,
  idle: pgPool.idleCount,
  waiting: pgPool.waitingCount,
});

重点看等待期间的变化:

观察结果下一步检查
waitingCount 持续大于 0,连接数达到上限是否有连接未释放、长事务或嵌套数据库调用
正在建立连接但迟迟没有可用连接连接建立、认证及连接超时是否生效
连接已被借出,但对应 SQL 未见记录驱动执行、Prisma 调用路径及数据库日志覆盖范围

这些计数反映的是整个 Pool 的状态。存在并发请求时,需要结合请求标识判断,不能把所有连接都归因于当前 callback。

第二步:在同一个 callback 中分段探测

普通 API route 写入成功,并不足以反映 OAuth callback 中的执行条件。可以在受控测试中,在 Account 写入前加入一次直接使用同一个 Pool 的探测:

import { pgPool } from "./prisma";

async function probeDatabase() {
  const started = Date.now();
  console.info("[db probe] before connect");

  const client = await pgPool.connect();
  console.info("[db probe] connected", {
    elapsedMs: Date.now() - started,
  });

  try {
    const result = await client.query(
      "SELECT pg_backend_pid() AS pid"
    );

    console.info("[db probe] query completed", {
      elapsedMs: Date.now() - started,
      pid: result.rows[0]?.pid,
    });
  } finally {
    client.release();
  }
}

这段代码会增加一次数据库往返,仅用于定位。它也可能改变连接预热和时序,因此需要与未加探测的情况对照。

根据停止位置继续处理:

  1. 停在 before connect 之后:优先查连接获取和建立过程。结合 Pool 状态区分等待空闲连接与新建连接。
  2. 已经输出 connected,查询没有完成:用记录下来的 PostgreSQL PID 关联数据库活动、等待事件和日志,检查该连接实际在等待什么。
  3. 直接 pg 查询成功,Prisma 调用仍卡住:再比较同一位置的 Prisma 简单查询、$executeRaw 和 account.create(),逐步缩小到 Prisma/driver adapter 路径或具体写入。

探测连接必须在后续 Prisma 操作开始前释放。否则,在连接数很小的配置下,诊断代码自己也可能占住后续操作需要的连接。

第三步:检查 callback 上下文和部署产物

沿着已经出现差异的路径排查,暂时不必反复调整连接池大小:

  • 确认 Route Handler 的实际 runtime。对使用 Node.js pg 的路由,检查是否按预期运行在 Node.js runtime;如需明确指定,可在实际的 route.ts 中设置 export const runtime = "nodejs"。
  • 检查连接生命周期。是否有代码调用了共享 Pool 的 end()、提前执行 $disconnect(),或手动借出连接后漏掉 release()?
  • 检查数据库调用顺序。是否有 callback、包装器、Prisma 扩展或事务持有连接,同时等待另一个需要连接的操作?这只是待验证方向,当前材料没有证明存在这种情况。
  • 确认数据库目标一致。比较 callback 与普通 API route 使用的环境变量来源、数据库、schema、连接参数和部署环境,记录时不要输出完整连接字符串或密码。
  • 检查依赖与生成步骤。当前材料没有给出 prisma、@prisma/client 和 pg 的完整版本。核对锁文件、构建日志及自定义生成目录,确认 Vercel 使用的是本次构建对应的 Prisma Client。

可以先查看安装关系:

npm ls next-auth @auth/prisma-adapter prisma @prisma/client @prisma/adapter-pg pg

这些检查只能确认配置和依赖事实,不能单凭“版本看起来一致”就判定运行时没有问题。若要判断是否属于某个已知缺陷,还需要能对应到具体版本、触发条件和最小复现的依据;当前材料没有提供这些证据。

是否需要 PgBouncer 或 Prisma Accelerate?

只有定位到连接数量或连接管理问题后,才能判断引入外部连接管理方案是否合适。

如果观测表明多实例并发导致 PostgreSQL 连接压力过大,可以评估外部连接池。如果应用没有释放连接、callback 中存在相互等待,或部署产物不一致,增加连接管理层未必能解决问题。

pg Pool 的存在本身也不能证明它与 cold starts 不兼容。应分别比较新实例和已运行实例的连接耗时与失败位置,确认故障是否真的只随冷启动出现。

超时和日志还需要注意什么?

Promise.race() 超时后,底层数据库操作不会因此自动取消。应用可能已经返回错误,而查询仍在继续等待或稍后完成。排查时不要紧接着重试相同的 Account INSERT,以免混淆多次请求的结果。

超时包装还应在 finally 中清理计时器;连接获取超时、客户端查询超时和数据库端语句超时需要分别处理,不能用一个外层计时器代替。

原代码会输出整个 account 对象,其中可能包含 OAuth token。诊断日志只保留请求标识、阶段、耗时和必要的连接池状态即可。

选择连接管理、依赖调整或 callback 代码方面的修复,需要同一次 OAuth callback 的三项证据:连接是否成功借出、直接 pg 查询是否返回,以及 Prisma 写入开始后对应数据库连接的状态。

备注:内容仅供参考。