Stripe Webhook调用失败排查:Next.js API端点出现504 FUNCTION_INVOCATION_TIMEOUT错误
Let's break down why you're hitting this timeout issue, even though your business logic (creating orders and updating ads) works correctly:
Core Issues
You're not sending a complete response to Stripe
In your current code, when handlingcheckout.session.completed, you havethen(() => res.status(200))—this only sets the status code but doesn't actually send the response back to Stripe. Stripe waits for a full HTTP response until it hits the timeout limit.DynamoDB operations use callbacks instead of async/await
YourcreateOrderfunction uses callback-based DynamoDB methods, which means the function resolves immediately (before the DB operations finish) but your response logic still doesn't send a proper reply.Next.js API route requires explicit response termination
Next.js API routes won't end automatically—you have to explicitly send a response (like.json()or.end()) or the function will run until it times out.
Step-by-Step Fix
1. Prioritize sending a fast response to Stripe
Stripe recommends responding to webhooks within 10 seconds, and you don't need to wait for business logic to finish. Send the success response first, then handle the DB work asynchronously.
2. Rewrite DynamoDB operations to use async/await
Callback-based code is harder to reason about and can lead to unexpected timing issues. Switch to using .promise() with async/await for cleaner, more reliable code.
3. Ensure proper response flow
Make sure you send the response before running long-running tasks, and explicitly return after sending to avoid extra code execution.
Here's the revised code:
import { buffer } from "micro" const AWS = require("aws-sdk") AWS.config.update({ accessKeyId: process.env.MY_AWS_ACCESS_KEY_ID, secretAccessKey: process.env.MY_AWS_SECRET_ACCESS_KEY, region: process.env.MY_AWS_REGION, endpoint: process.env.MY_AWS_ENDPOINT, }) const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY) const endpointSecret = process.env.STRIPE_CREATE_ORDER_SIGNING_SECRET // Rewrite createOrder with async/await for DynamoDB const createOrder = async (session) => { console.log("create order - session.id: ", session.id) console.log("create order - session.metadata.userID: ", session.metadata.userID) console.log("create order - session.amount_total / 100: ", session.amount_total / 100) const docClient = new AWS.DynamoDB.DocumentClient() const date = new Date() // Create order with await const orderParams = { TableName: process.env.ORDER_TABLE_NAME, Item: { id: session.id, userID: session.metadata.userID, amount: session.amount_total / 100, adID: session.metadata.adID, createdAt: date.toISOString(), updatedAt: date.toISOString(), }, } try { const orderData = await docClient.put(orderParams).promise() console.log("Order put Success - " + JSON.stringify(orderData, null, 2)) } catch (err) { console.log("Order put err - " + JSON.stringify(err, null, 2)) } // Calculate TTL and update ad const ninetyDays = 1000 * 60 * 60 * 24 * 90 const currTime = Date.now() const ttlSeconds = Math.ceil((ninetyDays + currTime) / 1000) const adParams = { TableName: process.env.AD_TABLE_NAME, Key: { id: session.metadata.adID }, UpdateExpression: "set paid = :paid, expdate = :expdate", ExpressionAttributeValues: { ":paid": true, ":expdate": ttlSeconds, }, ReturnValues: "UPDATED_NEW", } try { const adData = await docClient.update(adParams).promise() console.log("UPDATE Ad Success - " + JSON.stringify(adData, null, 2)) } catch (err) { console.log("UPDATE Ad err - " + JSON.stringify(err, null, 2)) } } export default async (req, res) => { if (req.method === "POST") { const requestBuffer = await buffer(req) const payload = requestBuffer.toString() const sig = req.headers["stripe-signature"] let event // Verify Stripe webhook signature try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret) } catch (err) { console.log("ERROR", err.message) return res.status(400).send(`Webhook create Order error: ${err.message}`) } // Handle checkout.session.completed event if (event.type === "checkout.session.completed") { const session = event.data.object // 1. Send success response to Stripe FIRST to avoid timeout res.status(200).json({ received: true }) // 2. Run business logic asynchronously (doesn't block the response) try { await createOrder(session) } catch (err) { console.error(`Create Order Error - ${err.message}`) // Add error alerting here if needed (no need to reply to Stripe again) } // Exit early to prevent extra code from running return } // Default response for other event types res.status(200).json({ received: true }) } else { // Handle non-POST requests res.setHeader("Allow", "POST") res.status(405).end("Method Not Allowed") } } export const config = { api: { bodyParser: false, externalResolver: true, }, }
Key Changes Explained
- Fast response first: By sending
res.status(200).json({ received: true })before runningcreateOrder, you ensure Stripe gets a reply immediately, avoiding the timeout. - Async/await for DynamoDB: Using
.promise()with await makes the code easier to debug and ensures you properly catch errors in DB operations. - Explicit return: After sending the response, we return early to prevent any unintended code execution that could delay the response.
Extra Tips
- Log response timing: Add a log when you send the response to confirm it's being sent quickly.
- Use async task queues for long tasks: If your business logic ever takes longer than a few seconds, use a queue like AWS SQS + Lambda to handle it in the background.
- Test with Stripe CLI: Use the Stripe CLI to test your webhook locally and simulate slow responses to verify your fix works.
内容的提问来源于stack exchange,提问作者Janis Basis Basovs

