Så minskade vi redirect-latensen 10x genom att sätta KV framför D1

Så minskade vi redirect-latensen 10x genom att sätta KV framför D1

20 april 2026

I vårt förra inlägg argumenterade vi för D1 framför KV för Shrtnr. Den relationella modellen vann eftersom vi behövde frågbar analys, inte bara slug-till-URL-uppslag.

Det resonemanget håller fortfarande. Men det visade sig vara bara hälften av arkitekturen vi i slutänden ville ha. Varje omdirigering betalade en round-trip från edge till en D1-primär i en enda region. För ett verktyg vars hela jobb är att komma ur vägen snabbt är det fel latensbudget.

Så vi lade till KV som en write-through-cache framför den. D1 är fortfarande källan till sanning; KV hanterar den heta sökvägen för omdirigeringar. Det är biten vi skulle ha levererat från dag ett om vi hade varit ärliga om vad en omdirigering behöver jämfört med vad resten av appen behöver.

Flaskhalsen: D1 är en databas i en enda region

Cloudflare Workers körs på 300+ edge-platser. D1 gör det inte. En D1-databas har en enda primär, placerad i ett datacenter. När en Worker i Jakarta hanterar en omdirigering korsar SQL-frågan vilken geografisk distans som än skiljer den edge-platsen från D1-primären, och round-trip:en sker innan 301-svaret kan skickas.

SQL-varaktigheten i sig är snabb: Cloudflare dokumenterar indexerade läsningar under en millisekund. Men SQL-varaktighet är inte request-varaktighet. Workern öppnar en anslutning, skickar frågan, väntar på svaret och parsar det. Nätverkslatens mellan edge och D1-primären dominerar. Realistiska siffror: 30–200ms per omdirigering beroende på geografi.

För klickspårning hade vi redan rätt mönster. Klickskrivningar körs i bakgrunden via ctx.waitUntil() efter att omdirigeringen har skickats, så att registrera analys aldrig blockerar besökaren. Men slug-upplösning, att svara på "vart pekar den här korta länken egentligen?", skedde på request-sökvägen. Och den request-sökvägen ägde D1 round-trip:en.

Fixen: KV som en edge-cache för slug-upplösning

Cloudflare KV är ett key-value-lager som cachar värden på den edge-plats som läser dem. Första läsningen från en viss region hämtar från centrallagringen; efterföljande läsningar från samma region serveras från den lokala cachen med latens under en millisekund till enstaka millisekunder. Ingen round-trip mellan regioner.

Förändringen: lagra slug-till-URL-mappningar i KV. Använd KV som första uppslag vid varje omdirigering. Fall tillbaka till D1 bara vid cache-miss. Behåll D1 som källan till sanning för allt annat: länkhantering, klickanalys, adminoperationer.

Vad KV lagrar per slug

Varje slug är den råa KV-nyckeln, inget prefix, ingen namespace. Värdet är en liten JSON-blob, ungefär 80–120 byte:

{
  "url": "https://destination.com/page",
  "disabled_at": null,
  "expires_at": null
}

Det räcker för att lösa omdirigeringen vid edge. En KV-läsning ersätter en D1-fråga.

KV stöder time-to-live på nycklar, vilket var frestande för utgångna länkar. Vi använde det inte. Länkar kan återaktiveras. En användare sätter expires_at tillbaka till null, och slugen behöver resolva igen. Om KV-nyckeln hade förfallit och tagits bort skulle återaktivering innebära att återskapa posten från D1. Att lagra tidsstämpeln inne i värdet och kontrollera den i kod håller varje tillståndsövergång enkel och reversibel.

Av samma anledning behåller vi inaktiverade slugs i KV istället för att radera dem. Om en inaktiverad slug saknas i KV faller omdirigeringshanteraren tillbaka till D1 vid varje förfrågan. En bot som hammrar en inaktiverad slug skulle generera obegränsad D1-trafik. Att behålla disabled_at i KV-värdet låter hanteraren returnera 404 vid edge utan att röra D1.

Omdirigeringssökvägen: före och efter

Före var varje omdirigering samma sekvens:

Request anländer vid edge
  → D1-fråga (round-trip mellan regioner)
  → Kontrollera inaktiverad / utgången
  → 301-omdirigering
  → waitUntil: registrera klick i D1

Varje omdirigering betalade D1-latenskostnaden. Inget cache-lager.

Efter rörs D1 bara om KV missar:

Request anländer vid edge
  → KV-läsning (lokal edge, under en millisekund för cachade nycklar)
  → [KV-hit] Kontrollera inaktiverad / utgången → 301-omdirigering
  → [KV-miss] D1-fallback → populera KV → 301-omdirigering
  → waitUntil: registrera klick i D1

Vid en KV-hit rörs D1 aldrig för omdirigeringen. Klickregistrering går fortfarande till D1 via ctx.waitUntil() och blockerar fortfarande inte svaret. Klickskrivningen sker efter att besökaren redan har skickats till destinationen.

Write-through: att hålla KV och D1 konsistenta

KV är eventually consistent. Skrivningar sprids globalt inom ungefär 60 sekunder. För en URL-förkortare är det ett acceptabelt fönster. Det är samma storleksordning som DNS-propagering, och strikt svagare än fördröjningen vi tolererar för de flesta admin-operationer. En nyskapad länk kanske inte omdirigeras vid varje edge på upp till en minut. En inaktiverad länk kanske fortfarande omdirigeras i upp till en minut på vissa platser. Ingetdera är en korrekthetsöverträdelse; båda är begränsade och korta.

För att hålla det fönstret så snävt som KV själv tillåter skriver varje mutation igenom synkront. D1 skrivs först. Det är källan till sanning. Sedan uppdateras KV innan API-svaret skickas. Ingen waitUntil() på cache-skrivningen. Om KV-put misslyckas misslyckas förfrågan. Om den lyckas startar konsistensklockan vid D1-committen, inte minuter senare när något asynkront jobb hinner ikapp.

Varje mutation som ändrar en slugs tillstånd (skapa, uppdatera, inaktivera, aktivera, radera) går genom ett servicelager. Admin-API, publikt API och MCP-verktygen som våra AI-flöden anropar går alla genom samma funktioner. Cache-konsistens hanteras på ett ställe, inte utspritt över request-hanterare.

Read-through: migrering utan driftstopp

Vi bulk-importerade inte befintliga slugs till KV när vi levererade cachen. Det finns inget migreringsskript, inget "värm cachen"-deploysteg. Istället populerar omdirigeringshanteraren KV vid miss. Första förfrågan till vilken förbefintlig slug som helst betalar D1-kostnaden och skriver posten till KV. Varje förfrågan efter det träffar cachen.

Operativt är det den minst riskfyllda förändringen vi kunde ha gjort. Produktionstrafik värmer cachen allt eftersom den anländer. Det finns inget ögonblick där systemet är mitt i en migrering och serverar föråldrade eller saknade data. Rollback är trivialt: ta bort KV-bindningen och hanteraren fortsätter använda D1-fallback-sökvägen den alltid har haft.

Prestandapåverkan

Vid en cache-hit sjunker slug-upplösningssteget från 30–200ms (D1 round-trip) till 1–5ms (KV-läsning vid den lokala edgen). Det är ungefär en 10x till 50x latensminskning, beroende på hur långt edge-platsen är från D1-primären. För besökaren är det gapet mellan att klicka på en länk och att se destinationssidan börja laddas.

D1-frågevolymen sjunker med den. KV:s standard-cache-TTL är 60 sekunder. För vilken slug som helst som nås mer än en gång per minut från samma region närmar sig hit-frekvensen 100%. För en slug som nås en gång från många regioner är värsta fallet en D1-fråga per Cloudflare-plats: ungefär 300 förfrågningar för att värma upp allt, jämfört med en fråga per klick. Konservativt: 90–99% färre omdirigeringsfrågor mot D1.

Vad som stannar på D1

KV tog över ett specifikt jobb: svara på "vart pekar den här slugen?" vid edge. Allt annat körs fortfarande på D1.

Klickregistrering skriver fortfarande till D1 via ctx.waitUntil(). Varje analysfråga körs fortfarande som en D1-aggregering: klick per land, hänvisare, enhet, webbläsare, OS och tidsintervall. Varje adminoperation (skapa länkar, redigera destinationer, hantera API-nycklar, söka) går fortfarande genom D1. KV kan inte svara på "visa mig de tio främsta hänvisarna för den här länken under de senaste 30 dagarna." Det var aldrig meningen.

Vi förkastade KV-endast, inte KV-som-cache

Om du läser vårt tidigare arkitekturinlägg noga var argumentet snävare än det lät. KV "faller samman" när du behöver analys, eftersom KV inte kan göra aggregering. Det är fortfarande sant. Vad vi inte sa uttryckligen, och vad write-through-cachen gör explicit, är att "kan inte göra analys" är ett uttalande om KV som databas, inte KV som cache.

För den heta sökvägen för omdirigeringar är KV rätt verktyg. Det är globalt replikerat, cachat vid edge, optimerat för läsintensiva arbetsbelastningar med glesa skrivningar. Det beskriver URL-omdirigeringar nästan perfekt. För analys är D1 fortfarande rätt verktyg: relationellt, frågbart, indexerbart över dimensioner. Att använda var och en för det den gör bäst är bättre än att välja en och kompromissa.

Det första inläggets slutsats står sig: "D1 är verktyget som passar" för en produkt som behöver både snabba uppslag och frågbar analys. Det som har förändrats är att vi lade till ett andra verktyg för den första halvan av det jobbet. Snabba uppslag behöver inte SQL; de behöver en cache som lever vid edge.

Vad händer härnäst

Det täcker arkitekturen som den står nu. Produktionsops-inlägget vi lovade är fortfarande på väg. Tre autentiseringssystem i en enda Worker, deployment med ett klick, TypeScript SDK:n, och vad det kostar att köra detta. Cache-lagret ändrar inget av det; det gör bara omdirigeringssökvägen tystare.

Om Oddbit

Vi är ett senior mjukvaruutvecklingsteam baserat på Bali, Indonesien, med rötter i Sverige. Vi bygger skräddarsydd programvara för startups och scale-ups: webbapplikationer, mobilappar, SaaS-plattformar och integrationerna som kopplar ihop dem. Våra kunder spänner över Norden, Europa och Sydostasien.

Shrtnr är ett av flera öppen källkod-verktyg vi har byggt och släppt. Andra inkluderar Tanam (ett Firebase-CMS med AI-innehållsgenerering), Flutter-paket med ungefär hundra tusen nedladdningar på pub.dev, och Firebase Genkit-plugins för AI-modellintegration.

Om du arbetar med ett tekniskt arkitekturbeslut, utvärderar edge computing för din produkt, eller bygger något från grunden, prata med oss. Vi visar hellre vårt arbete än berättar om det.

Google Cloud Partner

Berätta om ditt projekt