skip to content
Logo
Martin ANDRE

Creating a Serverless URL Shortener with Cloudflare

/ 4 min read

Let’s make a URL shortener using Cloudflare’s serverless tools and Rust! We’ll build a quick (more on that below), stable service that runs close to users.

The Main Parts

Cloudflare Workers

These are small programs that run on Cloudflare’s network of servers. They work like mini-computers spread across more than 200 cities around the world.

D1

This is Cloudflare’s SQLite database that runs on their servers. It’s great for storing our URL data because it:

  • Keeps data safe and stable
  • SQL query support
  • Replication across regions

KV (Key-Value)

This is Cloudflare’s distributed key-value store. We will use it as a cache to:

  • Lower database load
  • Improve responses time

Starting the Project

Let’s make a new Rust worker project:

Terminal window
# Install cargo-generate if you don't have it*
cargo binstall cargo-generate
# Create a new worker project*
cargo generate cloudflare/workers-rs
# Select templates/hello-world-http when prompted*

Add these packages to your Cargo.toml:

[dependencies]
worker = { version="0.5", features=["http", "d1"] }
worker-macros = { version="0.5", features=["http"] }
console_error_panic_hook = { version = "0.1" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Database Initialization

Make a schema.sql file (this is our “migration” file):

CREATE TABLE urls (
short_code TEXT PRIMARY KEY,
long_url TEXT NOT NULL
);
Terminal window
# Create D1 database
npx wrangler d1 create shortner-db
# Set up database tables
npx wrangler d1 execute shortner-db --file=./schema.sql --remote

Worker Code

Our worker does two things:

  1. Finds short URLs
  2. Sends users to the full URL
use serde::{Deserialize, Serialize};
use worker::*;
#[derive(Debug, Serialize, Deserialize)]
struct UrlEntry {
short_code: String,
long_url: String,
}
#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
console_error_panic_hook::set_once();
let router = Router::new();
router
.get_async("/ping", |_, _ctx| async move { Response::ok("pong") })
.get_async("/c/:code", |_req, ctx| async move {
let code = match ctx.param("code") {
Some(code) => code,
None => return Response::error("missing-code", 400),
};
let kv = ctx.kv("URL_CACHE")?;
// Quick check: look in KV store first
if let Ok(Some(url)) = kv.get(&code).text().await {
let destination_url = Url::parse(&url)?;
return Response::redirect_with_status(destination_url, 307);
}
let d1 = ctx.env.d1("DB_SHORTNER")?;
// Make the SQL query
let stmt = d1.prepare("SELECT * FROM urls WHERE short_code = ? LIMIT 1");
let query = stmt.bind(&[code.into()])?;
// Run the SQL query
match query.first::<UrlEntry>(None).await {
Ok(Some(entry)) => {
// Save to cache if found, keep for 24 hours
if let Err(e) = kv
.put(&entry.short_code, &entry.long_url)?
.expiration_ttl(60 * 60 * 24)
.execute()
.await
{
// Keep going if we can't cache
console_error!("Failed to cache URL({}): {:?}", &entry.long_url, e);
}
Response::redirect_with_status(Url::parse(&entry.long_url)?, 307)
}
Ok(None) => Response::error("not-found", 404),
Err(e) => {
console_warn!("Failed to fetch URL for code({}): {:?}", code, e);
Response::error("server-error", 500)
},
}
})
.run(req, env)
.await
}

wrangler.toml content
name = "blog-cloud-native-url-shortner"
main = "build/worker/shim.mjs"
compatibility_date = "2025-01-15"
[build]
command = "cargo install -q worker-build && worker-build --release"
[observability]
enabled = true
[[d1_databases]]
binding = "DB_SHORTNER"
database_name = "shortner-db"
database_id = "88c47054-70b3-4ca7-876b-1a2039f393ce"
[[kv_namespaces]]
binding = "URL_CACHE"
id = "883da662962c4effb27334664b68bcea"

The code works in steps:

  1. Checking the KV cache first
  2. Falling back to D1 if no cache exists
  3. Caching successful lookups for later requests

Deployment

Deploy your worker and create the needed resources:

Terminal window
# Create KV storage
npx wrangler kv:namespace create URL_CACHE
# Deploy the worker
npx wrangler deploy

Speed Test

Let’s check how fast it works:

Terminal window
curl -w "@curl-format.txt" https://XYZ.workers.dev/c/3def
time_namelookup: 0.148814s
time_connect: 0.168306s
time_appconnect: 0.200331s
time_pretransfer: 0.200521s
time_redirect: 0.000000s
time_starttransfer: 1.800326s
----------
time_total: 1.800535s

That’s slower than we want… Let’s try it again:

Terminal window
curl -w "@curl-format.txt" https://XYZ.workers.dev/c/3def
time_namelookup: 0.002433s
time_connect: 0.020938s
time_appconnect: 0.050296s
time_pretransfer: 0.050410s
time_redirect: 0.000000s
time_starttransfer: 0.103551s
----------
time_total: 0.103670s

This is way better! Now let’s try a new, unknown short code:

Terminal window
curl -w "@curl-format.txt" https://blog-cloud-native-url-shortner.martichou-andre.workers.dev/c/3defa
time_namelookup: 0.003037s
time_connect: 0.020590s
time_appconnect: 0.057306s
time_pretransfer: 0.057428s
time_redirect: 0.000000s
time_starttransfer: 0.247760s
----------
time_total: 0.248014s

curl-format.txt for the curious
Terminal window
time_namelookup: %{time_namelookup}s\n
time_connect: %{time_connect}s\n
time_appconnect: %{time_appconnect}s\n
time_pretransfer: %{time_pretransfer}s\n
time_redirect: %{time_redirect}s\n
time_starttransfer: %{time_starttransfer}s\n
----------\n
time_total: %{time_total}s\n

These tests tell us a lot. The first call triggers a cold start, which takes about 1.8 seconds. Subsequent requests are much faster. KV calls actually hit the network because KV data isn’t available in every location at first; however, they are cached afterwards.

In this example, a valid short code hits the KV store quickly, while an unknown code forces the system to query the D1 database to confirm its absence.

Keep in mind that on the free plan, workers might take a bit longer to cold start. Once running, all plans follow the same eviction policies.

For more details, see #1 and #2.

What’s Next

This basic URL shortener is not ready for production. But you may consider these improvements:

  • Add an admin API to create short URLs (link it to your D1 database)
  • Include analytics tracking (e.g., referrer, user-agent, IP location)
  • Set up CI/CD (using Cloudflare’s Git integration in the Worker settings)