Today, we’re excited to announce Workers KV is entering general availability and is ready for production use!
What is Workers KV?
Workers KV is a highly distributed, eventually consistent, key-value store that spans Cloudflare's global edge. It allows you to store billions of key-value pairs and read them with ultra-low latency anywhere in the world. Now you can build entire applications with the performance of a CDN static cache.
Why did we build it?
Workers is a platform that lets you run JavaScript on Cloudflare's global edge of 175+ data centers. With only a few lines of code, you can route HTTP requests, modify responses, or even create new responses without an origin server.
// A Worker that handles a single redirect,
// such a humble beginning...
addEventListener("fetch", event => {
event.respondWith(handleOneRedirect(event.request))
})
async function handleOneRedirect(request) {
let url = new URL(request.url)
let device = request.headers.get("CF-Device-Type")
// If the device is mobile, add a prefix to the hostname.
// (eg. example.com becomes mobile.example.com)
if (device === "mobile") {
url.hostname = "mobile." + url.hostname
return Response.redirect(url, 302)
}
// Otherwise, send request to the original hostname.
return await fetch(request)
}
Customers quickly came to us with use cases that required a way to store persistent data. Following our example above, it's easy to handle a single redirect, but what if you want to handle billions of them? You would have to hard-code them into your Workers script, fit it all in under 1 MB, and re-deploy it every time you wanted to make a change — yikes! That’s why we built Workers KV.
// A Worker that can handle billions of redirects,
// now that's more like it!
addEventListener("fetch", event => {
event.respondWith(handleBillionsOfRedirects(event.request))
})
async function handleBillionsOfRedirects(request) {
let prefix = "/redirect"
let url = new URL(request.url)
// Check if the URL is a special redirect.
// (eg. example.com/redirect/)
if (url.pathname.startsWith(prefix)) {
// REDIRECTS is a custom variable that you define,
// it binds to a Workers KV "namespace." (aka. a storage bucket)
let redirect = await REDIRECTS.get(url.pathname.replace(prefix, ""))
if (redirect) {
url.pathname = redirect
return Response.redirect(url, 302)
}
}
// Otherwise, send request to the original path.
return await fetch(request)
}
With only a few changes from our previous example, we scaled from one redirect to billions − that's just a taste of what you can build with Workers KV.
How does it work?
Distributed data stores are often modeled using the CAP Theorem, which states that distributed systems can only pick between 2 out of the 3 following guarantees:
Consistency - is my data the same everywhere?
Availability - is my data accessible all the time?
Partition tolerance - is my data resilient to regional outages?
Workers KV chooses to guarantee Availability and Partition tolerance. This combination is known as eventual consistency, which presents Workers KV with two unique competitive advantages:
Reads are ultra fast (median of 12 ms) since its powered by our caching technology.
Data is available across 175+ edge data centers and resilient to regional outages.
Although, there are tradeoffs to eventual consistency. If two clients write different values to the same key at the same time, the last client to write eventually "wins" and its value becomes globally consistent. This also means that if a client writes to a key and that same client reads that same key, the values may be inconsistent for a short amount of time.
To help visualize this scenario, here's a real-life example amongst three friends:
Suppose Matthew, Michelle, and Lee are planning their weekly lunch.
Matthew decides they're going out for sushi.
Matthew tells Michelle their sushi plans, Michelle agrees.
Lee, not knowing the plans, tells Michelle they're actually having pizza.
An hour later, Michelle and Lee are waiting at the pizza parlor while Matthew is sitting alone at the sushi restaurant — what went wrong? We can chalk this up to eventual consistency, because after waiting for a few minutes, Matthew looks at his updated calendar and eventually finds the new truth, they're going out for pizza instead.
While it may take minutes in real-life, Workers KV is much faster. It can achieve global consistency in less than 60 seconds. Additionally, when a Worker writes to a key, then immediately reads that same key, it can expect the values to be consistent if both operations came from the same location.
When should I use it?
Now that you understand the benefits and tradeoffs of using eventual consistency, how do you determine if it's the right storage solution for your application? Simply put, if you want global availability with ultra-fast reads, Workers KV is right for you.
However, if your application is frequently writing to the same key, there is an additional consideration. We call it "the Matthew question": Are you okay with the Matthews of the world occasionally going to the wrong restaurant?
You can imagine use cases (like our redirect Worker example) where this doesn't make any material difference. But if you decide to keep track of a user’s bank account balance, you would not want the possibility of two balances existing at once, since they could purchase something with money they’ve already spent.
What can I build with it?
Here are a few examples of applications that have been built with KV:
Mass redirects - handle billions of HTTP redirects.
User authentication - validate user requests to your API.
Translation keys - dynamically localize your web pages.
Configuration data - manage who can access your origin.
Step functions - sync state data between multiple APIs functions.
Edge file store - host large amounts of small files.
We’ve highlighted several of those use cases in our previous blog post. We also have some more in-depth code walkthroughs, including a recently published blog post on how to build an online To-do list with Workers KV.
What's new since beta?
By far, our most common request was to make it easier to write data to Workers KV. That's why we're releasing three new ways to make that experience even better:
1. Bulk Writes
If you want to import your existing data into Workers KV, you don't want to go through the hassle of sending an HTTP request for every key-value pair. That's why we added a bulk endpoint to the Cloudflare API. Now you can upload up to 10,000 pairs (up to 100 MB of data) in a single PUT request.
curl "https://api.cloudflare.com/client/v4/accounts/ \
$ACCOUNT_ID/storage/kv/namespaces/$NAMESPACE_ID/bulk" \
-X PUT \
-H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
-H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" \
-d '[
{"key": "built_by", value: "kyle, alex, charlie, andrew, and brett"},
{"key": "reviewed_by", value: "joaquin"},
{"key": "approved_by", value: "steve"}
]'
Let's walk through an example use case: you want to off-load your website translation to Workers. Since you're reading translation keys frequently and only occasionally updating them, this application works well with the eventual consistency model of Workers KV.
In this example, we hook into Crowdin, a popular platform to manage translation data. This Worker responds to a /translate
endpoint, downloads all your translation keys, and bulk writes them to Workers KV so you can read it later on our edge:
addEventListener("fetch", event => {
if (event.request.url.pathname === "/translate") {
event.respondWith(uploadTranslations())
}
})
async function uploadTranslations() {
// Ask crowdin for all of our translations.
var response = await fetch(
"https://api.crowdin.com/api/project" +
"/:ci_project_id/download/all.zip?key=:ci_secret_key")
// If crowdin is responding, parse the response into
// a single json with all of our translations.
if (response.ok) {
var translations = await zipToJson(response)
return await bulkWrite(translations)
}
// Return the errored response from crowdin.
return response
}
async function bulkWrite(keyValuePairs) {
return fetch(
"https://api.cloudflare.com/client/v4/accounts" +
"/:cf_account_id/storage/kv/namespaces/:cf_namespace_id/bulk",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Auth-Key": ":cf_auth_key",
"X-Auth-Email": ":cf_email"
},
body: JSON.stringify(keyValuePairs)
}
)
}
async function zipToJson(response) {
// ... omitted for brevity ...
// (eg. https://stuk.github.io/jszip)
return [
{key: "hello.EN", value: "Hello World"},
{key: "hello.ES", value: "Hola Mundo"}
]
}
Now, when you want to translate a page, all you have to do is read from Workers KV:
async function translate(keys, lang) {
// You bind your translations namespace to the TRANSLATIONS variable.
return Promise.all(keys.map(key => TRANSLATIONS.get(key + "." + lang)))
}
2. Expiring Keys
By default, key-value pairs stored in Workers KV last forever. However, sometimes you want your data to auto-delete after a certain amount of time. That's why we're introducing the expiration
and expirationTtl
options for write operations.
// Key expires 60 seconds from now.
NAMESPACE.put("myKey", "myValue", {expirationTtl: 60})
// Key expires if the UNIX epoch is in the past.
NAMESPACE.put("myKey", "myValue", {expiration: 1247788800})
# You can also set keys to expire from the Cloudflare API.
curl "https://api.cloudflare.com/client/v4/accounts/ \
$ACCOUNT_ID/storage/kv/namespaces/$NAMESPACE_ID/ \
values/$KEY?expiration_ttl=$EXPIRATION_IN_SECONDS"
-X PUT \
-H "X-Auth-Key: $CLOUDFLARE_AUTH_KEY" \
-H "X-Auth-Email: $CLOUDFLARE_AUTH_EMAIL" \
-d "$VALUE"
Let's say you want to block users that have been flagged as inappropriate from your website, but only for a week. With an expiring key, you can set the expire time and not have to worry about deleting it later.
In this example, we assume users and IP addresses are one of the same. If your application has authentication, you could use access tokens as the key identifier.
addEventListener("fetch", event => {
var url = new URL(event.request.url)
// An internal API that blocks a new user IP.
// (eg. example.com/block/1.2.3.4)
if (url.pathname.startsWith("/block")) {
var ip = url.pathname.split("/").pop()
event.respondWith(blockIp(ip))
} else {
// Other requests check if the IP is blocked.
event.respondWith(handleRequest(event.request))
}
})
async function blockIp(ip) {
// Values are allowed to be empty in KV,
// we don't need to store any extra information anyway.
await BLOCKED.put(ip, "", {expirationTtl: 60*60*24*7})
return new Response("ok")
}
async function handleRequest(request) {
var ip = request.headers.get("CF-Connecting-IP")
if (ip) {
var blocked = await BLOCKED.get(ip)
// If we detect an IP and its blocked, respond with a 403 error.
if (blocked) {
return new Response({status: 403, statusText: "You are blocked!"})
}
}
// Otherwise, passthrough the original request.
return fetch(request)
}
3. Larger Values
We've increased our size limit on values from 64 kB
to 2 MB
. This is quite useful if you need to store buffer-based or file data in Workers KV.
Consider this scenario: you want to let your users upload their favorite GIF to their profile without having to store these GIFs as binaries in your database or managing another cloud storage bucket.
Workers KV is a great fit for this use case! You can create a Workers KV namespace for your users’ GIFs that is fast and reliable wherever your customers are located.
In this example, users upload a link to their favorite GIF, then a Worker downloads it and stores it to Workers KV.
addEventListener("fetch", event => {
var url = event.request.url
var arg = request.url.split("/").pop()
// User sends a URI encoded link to the GIF they wish to upload.
// (eg. example.com/api/upload_gif/)
if (url.pathname.startsWith("/api/upload_gif")) {
event.respondWith(uploadGif(arg))
// Profile contains link to view the GIF.
// (eg. example.com/api/view_gif/)
} else if (url.pathname.startsWith("/api/view_gif")) {
event.respondWith(getGif(arg))
}
})
async function uploadGif(url) {
// Fetch the GIF from the Internet.
var gif = await fetch(decodeURIComponent(url))
var buffer = await gif.arrayBuffer()
// Upload the GIF as a buffer to Workers KV.
await GIFS.put(user.name, buffer)
return gif
}
async function getGif(username) {
var gif = await GIFS.get(username, "arrayBuffer")
// If the user has set one, respond with the GIF.
if (gif) {
return new Response(gif, {headers: {"Content-Type": "image/gif"}})
} else {
return new Response({status: 404, statusText: "User has no GIF!"})
}
}
Lastly, we want to thank all of our beta customers. It was your valuable feedback that led us to develop these changes to Workers KV. Make sure to stay in touch with us, we're always looking ahead for what's next and we love hearing from you!
Pricing
We’re also ready to announce our GA pricing. If you're one of our Enterprise customers, your pricing obviously remains unchanged.
$0.50 / GB of data stored, 1 GB included
$0.50 / million reads, 10 million included
$5 / million write, list, and delete operations, 1 million included
During the beta period, we learned customers don't want to just read values at our edge, they want to write values from our edge too. Since there is high demand for these edge operations, which are more costly, we have started charging non-read operations per month.
Limits
As mentioned earlier, we increased our value size limit from 64 kB
to 2 MB
. We've also removed our cap on the number of keys per namespace — it's now unlimited. Here are our GA limits:
Up to 20 namespaces per account, each with unlimited keys
Keys of up to 512 bytes and values of up to 2 MB
Unlimited writes per second for different keys
One write per second for the same key
Unlimited reads per second per key
Try it out now!
Now open to all customers, you can start using Workers KV today from your Cloudflare dashboard under the Workers tab. You can also look at our updated documentation.
We're really excited to see what you all can build with Workers KV!