Updated

Serverless Functions and API Responses

Image of code with comments explaining that calling res.send(200) closes the serverless function

Serverless functions provide a lot of benefits in regards to scalability and cost. Most developers have a high level understanding of the differences between serverless deployment and containerized deployment - but sometimes we need to endure a little pain and suffering to connect the dots.

This is the story of one such struggle - I had a basic understanding of how serverless functions differ from my development environment, such as how to manage caching data and how my code would be bundled into functions. It makes since that sending a server response would close the function, but it took a few hours of debugging and frustration to bridge that gap between basic knowledge and practical application.

I had set up a Stripe webhook to listen to the payment_intent.succeeded event, and send a POST request to my endpoint whenever the event was called. This is the recommended way to handle post-payment events, since handling them client-side can lead to errors (for example if the user closes the browser before reaching the payment confirmation page). With my endpoint, the goal was to extract the metadata (products, name, email, etc.) I had previously stored in the Stripe PaymentIntent in the prepayment process, send that information to a Google sheets file for the client, and send out order confirmation emails via Sendgrid.

I made sure to follow Stripe's documentation very closely for both my Webhook response and for accepting payment. In the Stripe Webhook documentation they say:

So, logically, I was throwing up a res.send(200) as soon as I had the PaymentIntent, and then continued on with the rest of my code to do the things I outlined above. Apparently this suggestion - returning a response early before executing logic with the data - is not exclusive to Stripe. Other services, such as Slack, also recommend immediately returning a response before executing any logic.

On my local machine, the whole process worked great. I could simulate Webhook events from the Stripe CLI, the rows were added to the spreadsheet successfully after every test payment, and the emails were being sent in a timely manner. However, once I pushed my code to Vercel preview, it all just stopped working. The weird part was that I wasn't getting any errors. So I started adding logs everywhere and repeatedly making commits to update the preview site and watch the live function logs in Vercel.

After a few hours and a verbose console.log statement after just about every line of code, it still just looked like the function stopped executing right in the middle of saving the spreadsheet data. No errors or anything. After several cycles of hypothesizing and testing, I finally figured out that the serverless function was just stopping cold after the res.send(200). The only reason the code execution was stopping in the middle of saving the spreadsheet data was because that was the first "await" in the execution path. So I moved the res.send(200) to the end of it all - and finally got to breathe a nice sigh of relief when it all worked again.

Some Googling around indicates that not too many other people have had this problem, although there is this discussion about it, as well as my Reddit post on the subject, with the general consensus being that the best way to handle a situation like this would be to call another serverless function to do the complex logic. For now, however, since my logging data to Google Sheets and sending an email via Sendgrid is not enough processing to cause a timeout for Stripe, I'll probably just leave it alone.

So take note and save yourself from hours of debugging - any code you write in a serverless function after a res.send() or a res.json() or anything else that calls res.end() under the hood might not be executed, especially if it's asynchronous. If documentation for the service you are using requires an immediate response before any logic, either start another serverless function with your logic, or cross your fingers that it executes quickly enough to avoid a timeout.