What is waitUntil (Vercel, Cloudflare) and when should I use it?
Tags: nextjs, webdev, react, serverless
Published: 2024-05-22 17:06:55

Recently, [Vercel announced](https://vercel.com/changelog/waituntil-is-now-available-for-vercel-functions) that their `waitUntil` utility method is now available for all serverless functions. Now, this useful feature is available for _both_ Vercel's Node.js and edge functions. This utility is not unique to Vercel -- [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil) also offers a similar one.

This is a very useful tool so let's dive into what `waitUntil` is and what it does, when to use it, and when it's better not to.

## What does `waitUntil` do

To understand what `waitUntil` does and why it exists, let's talk about how serverless functions work.

Serverless functions are designed to be [stateless](https://www.redhat.com/en/topics/cloud-native-apps/stateful-vs-stateless) and short-lived. Unlike traditional servers, serverless functions are not kept alive between requests. Instead, they are spun up on-demand to handle incoming requests and then shut down once the response is sent.

With JavaScript serverless functions, you have to `await` all asynchronous operations to ensure that the function doesn't return before the asynchronous operation is complete. If you don't `await` an asynchronous operation, the serverless function might be shut down before the operation is complete. If this happens, the operation might be canceled or fail which results in unpredictable behavior. This is why it's important to `await` all asynchronous operations in serverless functions.

```js
// ❌ BAD: If sendMetrics is an async function (a Promise), there is no guarantee it will succeed
async function POST(request) {
  const data = await createPost(request.body)
  sendMetrics({ metric: 'posts.created', increment: 1 })
  return new Response(data)
}
// ✅ GOOD: Awaiting will ensure it completes before the function returns
async function POST(request) {
  const data = await createPost(request.body)
  await sendMetrics({ metric: 'posts.created', increment: 1 })
  return new Response(data)
}
```

The downside of using `await` is that it can slow down the response time of your serverless functions. You ideally want to [keep the critical path of your request minimal](https://www.epicweb.dev/talks/improve-performance-and-reliability-of-your-api-with-events-and-background-functions) so you return the fastest response possible for your users. This is where `waitUntil` comes in.

`waitUntil` allows you to ensure that an async function (a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)) completes before the serverless function is shut down. As the example code above doesn't require any data returned from `sendMetrics`, we can use `waitUntil` to ensure that the async function completes in the background without blocking the response from returning to the user.

```js
// ✅ GOOD: The sendMetrics function will execute in the background,
// but not block the response from returning to the user
async function POST(request) {
  const data = await createPost(request.body)
  waitUntil(sendMetrics({ metric: 'posts.created', increment: 1 }))
  return new Response(data)
}
```

So, is `waitUntil` a solution to all possible problems? Well, it is a very useful tool and it ensures completion. However, it does not ensure _success_. For example, if the promise passed to `waitUntil` fails, the serverless function will still complete and return a response. So when should we use it?

## When to use `waitUntil`

`waitUntil` is useful when you need to perform some asynchronous work that is not critical to the response of the serverless function. This is useful for things like sending metrics, logging, cache control, or other things that should not block the user's response. The user should not pay the penalty for these operations, so they can be done in the background.

## When _not_ to use `waitUntil`

As mentioned above, `waitUntil` does not ensure _success_. If the promise passed to `waitUntil` fails, the serverless function will still complete and return a response. This is important to keep in mind when deciding when to use `waitUntil`.

In other words, `waitUntil` is not the tool for the job if the asynchronous operation affects critical business logic in your application (which is precisely what makes it great for logging and metrics!). If the operation fails, it's not the end of the world. But if the operation is critical to the response of the serverless function, `await` is the way to go.

## Can I run background jobs with `waitUntil`?

Short answer: No.

Long answer: `waitUntil` is not designed for running background jobs. There are no retries or tools for handling failures. Also, `waitUntil` will only run for as long as your function's timeout is set for, meaning if something takes longer than your timeout, it will be canceled. For running background jobs, you should use a system that handles queueing, retries, and logs failures.

Inngest is designed to handle background jobs reliably. It automatically handles retries, logs, and failures. If you need to run background jobs, you should consider using Inngest instead of `waitUntil`.

## Should I use `waitUntil` with Inngest?

Inngest functions are truly async, background jobs. To trigger Inngest functions, you use `inngest.send()` which sends the data to the Inngest _Event API_. Inngest then triggers the function and handles retries on it's end. The `inngest.send()` function returns a `Promise` so it is possible to use with `waitUntil`. Should you use it though?

```js
import { inngest } from "src/inngest";

async function POST(request) {
  const data = await createPost(request.body)
  waitUntil(inngest.send({ name: "post.created", data: data }))
  return new Response(data)
}
```

Typically, Inngest functions contain core business logic, so you need to ensure that the functions are triggered successfully. The key question is the reliability of `waitUntil` and the function it calls. Let's break down some things to consider:

* The Inngest Event API has very high uptime and is designed to be reliable.
* The `inngest.send()` function is very simple - mostly just an `POST` request to the Event API.
* The `waitUntil` utility from Vercel and Cloudflare is designed to be reliable.
* Networking issues or blips can always cause issues with outbound requests from any platform.
* `waitUntil` will only last as long as the timeout for the function.

Given these points, it is safe to use `waitUntil` with `inngest.send()` with the caveat that if there is a networking issue, the event may not reach Inngest. While this is a rare occurrence, it is a risk to be aware of and is a trade off that you need to decide for yourself.

If you don't want to take this risk, you can use `inngest.send()` without `waitUntil`, but it will be blocking:

```js
import { inngest } from "src/inngest";

async function POST(request) {
  const data = await createPost(request.body)
  await inngest.send({ name: "post.created", data: data })
  return new Response(data)
}
```

If you want **the best of both worlds**, you might determine a recovery strategy for when the event doesn't reach Inngest. This could be as simple as logging the failure and retrying the event later. Below is an incomplete example of how you might handle a failure and do something with the event. Not included is any logic you might need for retrying the event later using whatever logging backend or similar that you use.

```js
import { inngest } from "src/inngest";

async function POST(request) {
  const data = await createPost(request.body)
  waitUntil(
    inngest.send({ name: "post.created", data: data })})
      .catch((error) => {
        // Log the event somewhere where you could read and re-send it later
      })
  )
  return new Response(data)
}
```

## Conclusion

`waitUntil` is a highly useful tool to have in your serverless toolkit. It can help keep async code out of the critical path of the request to return the response to the user as fast as possible. It is very important to understand when to use it and when not to use it. It's also important to understand the risks of using it with other services.

Ideally, in the future, these services might offer some sort of out-of-the-box logging and retry mechanism for `waitUntil`. Until then, you should be aware of the risks and decide if it's right for your use case.