Using Workers KV to build an edge cached blog 🌍
Tags: javascript, webdev, serverless, showdev
Published: 2022-06-03 14:08:34

Last time I covered Cloudflare Workers I built a dev.to proxy to host the articles I write here on my personal site:

{% embed https://dev.to/bryce/web-workers-are-the-future-3nkg %}

It was a nice project but had some flaws, mainly performance. Cloudflare Workers boast about [0ms cold starts](https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/) and [low latency](https://workers.cloudflare.com/) but my little blog felt sluggish. So what'd I miss?

## A global issue 🗺

Since the Worker's `/post` route hits the dev.to API directly, there's no caching involved. This is one of Cloudflare's primary features so I thought there would be some layer baked in but there's nothing of the sort. The problem here is summed up pretty well by this tweet:

{% embed https://twitter.com/sambreed/status/1523317291764514816 %}

Even though the blog worker is globally distributed, because it hits a (central) API for each request the turnaround time makes running at the edge pointless! This is especially the case for me as I live in Europe and the round-trip time to load the page is palpable.

> If you haven't heard the term 'edge' before, it means a distributed network of servers that are geographically close to end users, enabling lower latency and faster load times. In this case, a request coming from Europe would hit a different server than one coming from the US but the content is the same.

I recently attended [JSWorld Amsterdam](https://frontenddeveloperlove.com/) and heard a talk that mentioned Workers KV which sounded exactly like the caching layer that would boost performance while still providing benefits of being at the edge. (And thankfully the free tier is more than sufficient for my needs.) This hypothesis was quickly verified by replacing the API call with a KV getter, decreasing time to first byte (server response delay) by ~10x!

From: 🐌

![slower ttfb](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6947c6ua3ha7e8xm3nc0.png)

To: 🐆

![faster ttfb](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kqum9cmjfvtnwl05xc1.png)

## Setting up KV 🗄

The steps to do this are pretty [simple](https://developers.cloudflare.com/workers/runtime-apis/kv/#kv-bindings). Using the wrangler CLI you instantiate a binding to a variable name:

```bash
$ wrangler kv:namespace create "POSTS"
```

And add it to `wrangler.toml`:

```java
kv_namespaces = [
  { binding = "POSTS", id = "asdfjkl12345" }
]
```

That variable is then available in your Worker and you can call get/put on it to retrieve/store data. With this I added a function to fetch post data, store it in the cache and track that it's been cached:

```js
export async function updateEdgeCacheForPost(id: number) {
  const post = await getPost(id)
  await POSTS.put(`${post.slug}`, JSON.stringify(post))

  const posts = await getCachedPosts()
  const cachedPostIndex = posts.map(p => p.id).indexOf(id)
  posts[cachedPostIndex].cached = true
  await POSTS.put('INDEX', JSON.stringify(posts))
}
```

Now `/:slug` routes can hit this cache directly:

```js
export async function getCachedPost(slug: string): Promise<PostDetailType> {
  const response = await POSTS.get(`${slug}`)
  return JSON.parse(response)
}
```

Then I replaced the `/post:id` route with a redirect that will permanently redirect to the cached version of the post:

```js
app.get('/post/:id', async (c) => {
  const id = Number(c.req.param('id'))
  const slug = await getCachedSlugById(id)
  return c.redirect(`/${slug}`, 301)
})
```

Subsequent index renders will pick up on the cached flag and render the link as /:slug, avoiding this redirect. I added an /update route (that requires a password matching an environment variable) that will update the index of posts.

> Note: since the dev.to API rate-limits requests (somewhere around ~1 a second) and the Worker runtime length limit is 10ms it's not possible to cache all data in one request. So on-demand caching is needed in lieu of batching. But once the post is cached it doesn't need to be again!

---

And now I can officially say it's cached at the edge! The index needs to be refreshed when I write a new post but that's as simple as loading the `/update` route and waiting a few seconds. Super happy with the result!

Can check out the code here (& fork to deploy your own):

{% embed https://github.com/brycedorn/blog %}