-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Init new blogpost * Minor grammatical tweaks * Format post and tweak some phrasing * Fix one sentence and add more narrative to the tracing visualizations * Vamp up intro and add a LinkCard * Remove my redundancy * A few code fixes and wording tweaks --------- Co-authored-by: Brett Beutell <brbeut@gmail.com>
- Loading branch information
Showing
7 changed files
with
178 additions
and
20 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 172 additions & 0 deletions
172
www/src/content/blog/2024-11-02-asynchronous-tasks-in-cloudflare-part1.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
--- | ||
title: "Handling Asynchronous Tasks in Cloudflare Workers – Part 1: Essentials for a Single Worker" | ||
description: Explore techniques for handling asynchronous tasks in Cloudflare Workers to ensure quick responses and seamless user experiences. | ||
slug: asynchronous-tasks-in-cloudflare-part1 | ||
date: 2024-11-02 | ||
author: Nele Uhlemann | ||
tags: | ||
- Hono.js | ||
- Cloudfalre Workers | ||
- Web development | ||
- asynchronous programming | ||
--- | ||
|
||
import { LinkCard } from "@astrojs/starlight/components"; | ||
import PackageManagers from "@/components/PackageManagers.astro"; | ||
|
||
In modern data APIs, asynchronous background tasks are not just common—they're fundamental to almost every operation. Whether it's database operations, sending emails, or processing webhooks, these behind-the-scenes tasks form the backbone of most services. | ||
|
||
Most importantly: The way you manage asynchronous operations in your API handlers can significantly impact your service's overall performance and reliability. | ||
|
||
When a request hits our API and we run all of its logic sequentially, the response can end up being unnecessarily delayed. | ||
This increases latency for the end user or, worse, can result in failed responses—such as when one of the asynchronous tasks encounters an error. | ||
|
||
Handling asynchronous tasks is like running a marathon for our system: | ||
it requires endurance, coordination, retries, and error handling at the right points. | ||
There are multiple ways to handle asynchronous tasks. In part 1 of this blog post series, I will explore how we can manage asynchronous tasks within a single Cloudflare Worker. | ||
|
||
## Example: A Marathon Registration API | ||
|
||
Imagine this: We are organizing a marathon and need to build an api to register participants. | ||
We set up a simple data API with [Hono](https://hono.dev/) on Cloudflare Workers, which stores the runners' information in a database. After a successful registration, we send a confirmation email with all the important details for the race. | ||
|
||
In this example, we will store registration data in a [Neon database](https://neon.tech/docs/introduction), and we'll use [Resend](https://resend.com/docs/introduction) to send out emails for us. | ||
|
||
<LinkCard | ||
title="Follow along on GitHub" | ||
description="Here is a repo with the code examples for this post" | ||
href="https://github.com/Nlea/cloudflare-handling-asynchronous-tasks-" | ||
icon="external" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
/> | ||
|
||
## Sequential Execution | ||
|
||
Let's start by looking at running all tasks sequentially. | ||
|
||
```typescript | ||
app.post("/api/marathon-sequential", async (c) => { | ||
const { firstName, lastName, email, address, distance } = await c.req.json(); | ||
|
||
await insertData( | ||
firstName, | ||
lastName, | ||
email, | ||
address, | ||
distance, | ||
c.env.DATABASE_URL | ||
); | ||
await sendMail(email, c.env.RESEND_API, firstName); | ||
|
||
return c.text("Thanks for registering for our Marathon", 200); | ||
}); | ||
``` | ||
|
||
In the code above, we first insert the runner's data into the database and then send the confirmation email. | ||
All the asynchronous tasks (functions) are called sequentially. | ||
Although this code works, it slows down the return to the user. | ||
|
||
If we look into the trace provided in [Fiberplane Studio](https://fiberplane.com/docs/get-started/), we see that tasks are running in sequential order, and the total response time depends on the whole sequence. | ||
The trace visualization helps us understand the performance impact of sequential execution on our application: | ||
|
||
![Sequential trace in Fiberplane](@/assets/blog/2024-11-02-sequential-trace.png) | ||
|
||
## Fire and Forget | ||
|
||
One way to return quickly is by not waiting for other tasks to complete successfully. | ||
Generally, this can be useful if, from a business perspective, it doesn’t matter whether the email is eventually sent or not. | ||
If you don’t need a guarantee that the email will be sent (e.g., for non-critical notifications), you can simply fire the promise and proceed without waiting for it. | ||
|
||
While it is essential for a successful registration to store the runner’s details in the database, we can argue that the email we send is not mission-critical, so we don’t need to wait for the promise to return. | ||
Taking this into account, we could modify the code like so: | ||
|
||
```typescript | ||
app.post("/api/marathon-fire-and-forget", async (c) => { | ||
const { firstName, lastName, email, address, distance } = await c.req.json(); | ||
|
||
await insertData( | ||
firstName, | ||
lastName, | ||
email, | ||
address, | ||
distance, | ||
c.env.DATABASE_URL | ||
); | ||
|
||
sendMail(email, c.env.RESEND_API, firstName).catch(console.error); | ||
|
||
return c.text("Thanks for registering for our Marathon", 201); | ||
}); | ||
``` | ||
|
||
However, Cloudflare Workers are ephemeral, and your Worker might terminate after you return a response. This means dangling promises may not finish successfully, which could result in the email never being sent. | ||
Therefore, when working with Cloudflare Workers, we should ensure they are built in a way that gives promises a chance to complete. | ||
|
||
As we can see here in the trace, the response returns after the database insert, but we cannot know whether the email was sent, because the worker shut down before the promise was resolved. The trace helps us identify potential issues with unfinished tasks in our fire-and-forget approach. | ||
|
||
![Fire and Forget trace in Fiberplane](@/assets/blog/2024-11-02-fire-and-forget.png) | ||
|
||
On another note: Do you want your support team assisting runners who didn't receive a confirmation email and are now worried they haven't signed up for the race? (Probably not!) | ||
|
||
## Concurrent Execution: Know Your Business Logic's Dependencies! | ||
|
||
If we want to return a response to the user while still performing background tasks in our Worker, we can use `executionCtx.waitUntil()`. | ||
This allows us to send a response immediately and handle tasks like email and database updates in the background. | ||
|
||
If there are multiple tasks to run in parallel, we can use `Promise.all()` to group them. | ||
|
||
```typescript | ||
const tasks = [ | ||
sendMail(email, c.env.RESEND_API, firstName), | ||
insertData(firstName, lastName, email, address, distance, c.env.DATABASE_URL) | ||
]; | ||
c.executionCtx.waitUntil(Promise.all(tasks)); | ||
``` | ||
|
||
If we now look at the trace, we see the response returns immediately, | ||
and after a short while, the client instrumentation also shows the other two tasks related to the trace. | ||
This method leads to the parallel execution of both tasks, | ||
and the trace clearly shows how this improves our response time compared to the sequential approach: | ||
|
||
![Parallel execution in Fiberplane](@/assets/blog/2024-11-02-parallel.png) | ||
|
||
However, in our example, we want to ensure that the user’s data is stored in the database before returning a response to the user. | ||
Without this step, we can’t confirm their registration in the system. | ||
So, we actually do have a dependency on the database insert and a required sequential flow. | ||
|
||
However, instead of waiting for the email promise before returning, we could use `waitUntil()` to keep the worker open and return as follows: | ||
|
||
```typescript | ||
app.post("/api/marathon-waituntil", async (c) => { | ||
const { firstName, lastName, email, address, distance } = await c.req.json(); | ||
|
||
await insertData( | ||
firstName, | ||
lastName, | ||
email, | ||
address, | ||
distance, | ||
c.env.DATABASE_URL | ||
); | ||
|
||
c.executionCtx.waitUntil(sendMail(email, c.env.RESEND_API, firstName)); | ||
|
||
return c.text("Thanks for registering for our Marathon", 201); | ||
}); | ||
``` | ||
|
||
Similar to the fire and forget trace, we see that the response is sent after the database insert, but the email is sent after the response is returned and starts _after_ the database insert finishes. | ||
The trace confirms our intended execution order, all while showing improved response times. | ||
|
||
![WaitUntil trace in Fiberplane](@/assets/blog/2024-11-02-waituntil.png) | ||
|
||
## Error handling and retries | ||
|
||
In our scenario, the approach using `waitUntil()` for sending the email is the most suitable. However, there are still a few more things to consider. | ||
|
||
What happens if sending the email fails? How can we ensure that tasks are retried if they encounter an error? | ||
While we could implement custom logic for retries and error handling, this isn’t always the most effective solution. | ||
|
||
This is where we could break down tasks across separate workers to streamline handling and retries. | ||
In the next part of this blog post series, we’ll explore how to manage asynchronous tasks across multiple workers and ensure tasks are retried if they fail. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters