We rely on technology to help us on a daily basis – if you are not good at keeping track of time, your calendar can remind you when it's time to prepare for your next meeting. If you made a reservation at a really nice restaurant, you don't want to miss it! You appreciate the app to remind you a day before your plans the next evening.
However, who tells the application when it's the right time to send you a notification? For this, we generally rely on scheduled events. And when you are relying on them, you really want to make sure that they occur. Turns out, this can get difficult. The scheduler and storage backend need to be designed with scale in mind - otherwise you may hit limitations quickly.
Workers, Durable Objects, and Alarms are actually a perfect match for this type of workload. Thanks to the distributed architecture of Durable Objects and their storage, they are a reliable and scalable option. Each Durable Object has access to its own isolated storage and alarm scheduler, both being automatically replicated and failover in case of failures.
There are many use cases where having a reliable scheduler can come in handy: running a webhook service, sending emails to your customers a week after they sign up to keep them engaged, sending invoices reminders, and more!
Today, we're going to show you how to build a scalable service that will schedule HTTP requests on a specific schedule or as one-off at a specific time as a way to guide you through any use case that requires scheduled events.
Quick intro into the application stack
Before we dive in, here are some of the tools we’re going to be using today:
Wrangler - CLI tool to develop and publish Workers
Cloudflare Workers - runtime
Cloudflare Durable Objects - storage for HTTP requests and Alarms to schedule them
The application is going to have the following components:
Scheduling system API to accept scheduled requests and manage Durable Objects
Unique Durable Object per scheduled request, each with
Storage - keeping the request metadata, such as URL, body, or headers.
Alarm - a timer (trigger) to wake Durable Object up.
While we will focus on building the application, the Cloudflare global network will take care of the rest – storing and replicating our data, and making sure to wake our Durable Objects up when the time's right. Let’s build it!
Initialize new Workers project
Get started by generating a completely new Workers project using the wrangler init
command, which makes creating new projects quick & easy.
wrangler init -y durable-objects-requests-scheduler
For more information on how to install, authenticate, or update Wrangler, check out https://developers.cloudflare.com/workers/wrangler/get-started
Preparing TypeScript types
From my personal experience, at least a draft of TypeScript types significantly helps to be more productive down the road, so let's prepare and describe our scheduled request in advance. Create a file types.ts in src directory and paste the following TypeScript definitions.
src/types.ts
export interface Env {
DO_REQUEST: DurableObjectNamespace
}
export interface ScheduledRequest {
url: string // URL of the request
triggerAt?: number // optional, unix timestamp in milliseconds, defaults to `new Date()`
requestInit?: RequestInit // optional, includes method, headers, body
}
A scheduled request Durable Object class & alarm
Based on our architecture design, each scheduled request will be saved into its own Durable Object, effectively separating storage and alarms from each other and allowing our scheduling system to scale horizontally - there is no limit to the number of Durable Objects we create.
In the end, the Durable Object class is a matter of a couple of lines. The code snippet below accepts and saves the request body to a persistent storage and sets the alarm timer. Workers runtime will wake up the Durable Object and call the alarm()
method to process the request.
The alarm method reads the scheduled request data from the storage, then processes the request, and in the end reschedules itself in case it's configured to be executed on a recurring schedule.
src/request-durable-object.ts
import { ScheduledRequest } from "./types";
export class RequestDurableObject {
id: string|DurableObjectId
storage: DurableObjectStorage
constructor(state:DurableObjectState) {
this.storage = state.storage
this.id = state.id
}
async fetch(request:Request) {
// read scheduled request from request body
const scheduledRequest:ScheduledRequest = await request.json()
// save scheduled request data to Durable Object storage, set the alarm, and return Durable Object id
this.storage.put("request", scheduledRequest)
this.storage.setAlarm(scheduledRequest.triggerAt || new Date())
return new Response(JSON.stringify({
id: this.id.toString()
}), {
headers: {
"content-type": "application/json"
}
})
}
async alarm() {
// read the scheduled request from Durable Object storage
const scheduledRequest:ScheduledRequest|undefined = await this.storage.get("request")
// call fetch on scheduled request URL with optional requestInit
if (scheduledRequest) {
await fetch(scheduledRequest.url, scheduledRequest.requestInit ? webhook.requestInit : undefined)
// cleanup scheduled request once done
this.storage.deleteAll()
}
}
}
Wrangler configuration
Once we have the Durable Object class, we need to create a Durable Object binding by instructing Wrangler where to find it and what the exported class name is.
wrangler.toml
name = "durable-objects-request-scheduler"
main = "src/index.ts"
compatibility_date = "2022-08-02"
# added Durable Objects configuration
[durable_objects]
bindings = [
{ name = "DO_REQUEST", class_name = "RequestDurableObject" },
]
[[migrations]]
tag = "v1"
new_classes = ["RequestDurableObject"]
Scheduling system API
The API Worker will accept POST HTTP methods only, and is expecting a JSON body with scheduled request data - what URL to call, optionally what method, headers, or body to send. Any other method than POST will return 405 - Method Not Allowed HTTP error.
HTTP POST /:scheduledRequestId?
will create or override a scheduled request, where :scheduledRequestId is optional Durable Object ID returned from a scheduling system API before.
src/index.ts
import { Env } from "./types"
export { RequestDurableObject } from "./request-durable-object"
export default {
async fetch(
request: Request,
env: Env
): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method Not Allowed", {status: 405})
}
// parse the URL and get Durable Object ID from the URL
const url = new URL(request.url)
const idFromUrl = url.pathname.slice(1)
// construct the Durable Object ID, use the ID from pathname or create a new unique id
const doId = idFromUrl ? env.DO_REQUEST.idFromString(idFromUrl) : env.DO_REQUEST.newUniqueId()
// get the Durable Object stub for our Durable Object instance
const stub = env.DO_REQUEST.get(doId)
// pass the request to Durable Object instance
return stub.fetch(request)
},
}
It's good to mention that the script above does not implement any listing of scheduled or processed webhooks. Depending on how the scheduling system would be integrated, you can save each created Durable Object ID to your existing backend, or write your own registry – using one of the Workers storage options.
Starting a local development server and testing our application
We are almost done! Before we publish our scheduler system to the Cloudflare edge, let's start Wrangler in a completely local mode to run a couple of tests against it and to see it in action – which will work even without an Internet connection!
wrangler dev --local
The development server is listening on localhost:8787, which we will use for scheduling our first request. The JSON request payload should match the TypeScript schema we defined in the beginning – required URL, and optional triggerEverySeconds
number or triggerAt
unix timestamp. When only the required URL is passed, the request will be dispatched right away.
An example of request payload that will send a GET request to https://example.com every 30 seconds.
{
"url": "https://example.com",
"triggerEverySeconds": 30,
}
> curl -X POST -d '{"url": "https://example.com", "triggerEverySeconds": 30}' http://localhost:8787
{"id":"000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f"}
From the wrangler logs we can see the scheduled request ID 000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f
is being triggered in 30s intervals.
Need to double the interval? No problem, just send a new POST request and pass the request ID as a pathname.
> curl -X POST -d '{"url": "https://example.com", "triggerEverySeconds": 60}' http://localhost:8787/000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f
{"id":"000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f"}
Every scheduled request gets a unique Durable Object ID with its own storage and alarm. As we demonstrated, the ID becomes handy when you need to change the settings of the scheduled request, or to deschedule them completely.
Publishing to the network
Following command will bundle our Workers application, export and bind Durable Objects, and deploy it to our workers.dev subdomain.
wrangler publish
That's it, we are live! ? The URL of your deployment is shown in the Workers logs. In a reasonably short period of time we managed to write our own scheduling system that is ready to handle requests at scale.
You can check full source code in Workers templates repository, or experiment from your browser without installing any dependencies locally using the StackBlitz template.
What to see or build next
New to Workers? Check our Get started guide.
Use Access or service bindings if you want to protect your API from unauthorized access.
Got an idea for a Worker, get started in seconds => https://workers.new/typescript (full list of StackBlitz supported templates)
Dive into more Workers examples