Het probleem van een koude start in traditioneel serverloos

In 2024 documenteerde een Datadog-analyse dat 40% van de Lambda-aanroepen in productie was lijdt aan een koude start langer dan 500 ms. Voor Python- of Java-functies loopt dit aantal op tot 1-3 seconden. De koude start is de tijd die verstrijkt vanaf het moment dat een verzoek binnenkomt tot het moment waarop de functie is daadwerkelijk klaar om deze te verwerken: dat is de tijd die nodig is om te starten een container, laad de runtime, initialiseer de afhankelijkheden.

Cloudflare Workers hanteert een radicaal andere aanpak: in plaats van containers gebruikt het V8 isolaten. Het meetbare resultaat: gemiddelde startup minder dan 1 ms, nul "koude starts" in de traditionele zin van het woord. Begrijpen waarom het nodig is om naar beneden te komen met details over de architectuur van de V8-motor en het procesisolatiemodel.

Wat je gaat leren

  • Wat is een V8-isolaat en hoe verschilt dit van een OS-proces of Docker-container
  • Waarom containers last hebben van structurele koudestarts en hoe deze zich manifesteren
  • Het uitvoeringsmodel van Cloudflare Workers: van verzoekroutering tot geïsoleerde pool
  • Snapshot V8: De techniek die JavaScript-initialisatiekosten elimineert
  • Geïsoleerde modelbeperkingen: CPU-tijd, geheugen, beschikbare API's
  • Benchmarkvergelijking: werknemers versus Lambda versus Lambda@Edge
  • Wanneer Workers gebruiken en wanneer containers de juiste keuze blijven

Containers, processen en isolaten: een taxonomie

Om isolaten te begrijpen, moet je eerst begrijpen wat ze vervangen. Elk niveau van de abstractie heeft verschillende opstartkosten:

Primitief Isolatie Typisch opstarten Geheugenoverhead Voorbeelden
VM (hypervisor) Hardware 10-60 seconden 512 MB - 2 GB EC2, GCE, Azure-VM
Containers (Linux-naamruimten) Kernel (cgroups + naamruimten) 100ms - 2s 50-500 MB Docker, Lambda, Cloud Run
OS-proces Kernel (PID, VAS) 10-100 ms 10-100 MB Node.js, Python-proces
V8 geïsoleerd Runtime (afzonderlijke heap, geen gedeeld geheugen) < 1 ms 1-10 MB Cloudflare-werknemers, Deno Deploy

Un V8 geïsoleerd is een geïsoleerd exemplaar van de V8 JavaScript-heap: het heeft de uw eigen heap-allocator, uw eigen JavaScript-objecten, uw eigen garbage collector. Twee isolaten ze delen geen JavaScript-geheugen en kunnen elkaar niet hinderen. Dit is het de basis van de veiligheidsisolatie van arbeiders, maar de architecturale consequentie is dat wel dat het maken van een isolaat een bewerking is die microseconden in beslag neemt, en geen milliseconden.

Waarom Lambda last heeft van een koude start

Wanneer er een verzoek binnenkomt voor een "koude" (niet warme) Lambda-functie zonder container vooraf toegewezen), moet AWS deze reeks uitvoeren:

# Sequenza di cold start Lambda (Node.js 20)
1. Allocazione risorse compute (EC2/Firecracker VM)      ~50-200ms
2. Download immagine container dal registro              ~50-300ms (dipende dalla dimensione)
3. Setup rete: VPC, ENI attachment (se VPC configurata) ~200-1000ms (!)
4. Avvio runtime Node.js                                 ~30-80ms
5. Caricamento dependencies (node_modules)               ~20-200ms
6. Esecuzione handler initialization code               ~10-500ms
7. Prima invocazione effettiva                           ----------

Totale cold start: 360ms - 2.28 secondi (VPC worst case: fino a 3-5s)

Firecracker (de microVM die door AWS wordt gebruikt) verminderde de opstarttijd van de VM tot ongeveer 125 ms, maar het structurele probleem blijft bestaan: elke uitvoeringsomgeving is een geïsoleerde container op kernelniveau, dat bij elke koude start helemaal opnieuw moet worden gestart.

V8 isolaten: arbeidersarchitectuur

Cloudflare Workers draait door arbeider, de open-source runtime uitgebracht door Cloudflare in 2022. Elke PoP (Point of Presence, er zijn er wereldwijd 300+) draait een vloot van werkende processen. Wanneer er een verzoek binnenkomt:

Sequenza di gestione richiesta in Cloudflare Workers:

1. Richiesta arriva al PoP Cloudflare piu vicino        ~0ms (BGP anycast routing)
2. Routing al processo workerd appropriato               ~0.1ms
3. Lookup/allocation dell'isolate per questo Worker      ~0.5ms (da pool precreato)
4. Esecuzione del fetch handler                          ~0.1ms overhead
5. Risposta

Totale "startup": < 1ms

Het belangrijkste punt is stap 3: workerd onderhoudt a verzameling isolaten vooraf geïnitialiseerd. Elk isolaat heeft de Worker-code al geladen dankzij V8-momentopnamen.

V8-snapshots: heap-status serialiseren

V8 ondersteunt heap-serialisatie in een binair formaat genaamd "snapshot". Wanneer zet een Worker in, Cloudflare:

  1. Voert de JavaScript-code van de Worker tijdelijk uit
  2. Laat de code de initialisatie voltooien (module-evaluatie)
  3. Serialiseer de isolateheap in een binaire momentopname
  4. Verdeelt deze momentopname naar PoP's
  5. Er worden nieuwe isolaten gemaakt op basis van deze momentopname (deserialisatie), en niet helemaal opnieuw
// Il tuo Worker ha questa struttura:
import { Router } from 'itty-router';

// Questo codice viene eseguito durante il "module evaluation"
// e serializzato nello snapshot
const router = new Router();

router.get('/users/:id', async ({ params, env }) => {
  const user = await env.DB.get(`user:${params.id}`);
  return Response.json({ user });
});

// Il fetch handler e il punto di ingresso per ogni richiesta
export default {
  fetch: router.fetch
};

// Quando un isolate viene creato dallo snapshot:
// - router e gia costruito e configurato
// - tutte le route sono gia registrate
// - NON c'e alcun costo di module evaluation per ogni richiesta

Dit is fundamenteel anders dan wat er in Lambda gebeurt: in Lambda bij elke koude start moet alle module-initialisatiecode opnieuw worden uitgevoerd. Bij werknemers, deze fase vond slechts één keer plaats op het moment van inzet.

Beveiligingsisolatie: V8 Sandbox

Een veelgehoord bezwaar is: "Als er meerdere werknemers in hetzelfde proces actief zijn, hoe dan?" zijn ze geïsoleerd?". Het antwoord is de V8-zandbak, een mechanisme meerlaags:

V8 isolatielagen

  • Hoopscheiding: Elk isolaat heeft een volledig afzonderlijke hoop. Het geheugen van een andere isolate is niet toegankelijk via JavaScript.
  • Geen gedeelde veranderlijke status: De globale variabelen van een werknemer zijn dat niet zichtbaar voor andere werknemers.
  • Gecontroleerde API's: Toegang tot gevaarlijke functionaliteit (bestandssysteem, network raw, process) wordt bestuurd door de workerd-runtime, niet door V8 zelf.
  • Sandbox V8: V8 bevat sandbox-mechanismen die dit voorkomen JavaScript om willekeurige machinecode uit te voeren of toegang te krijgen tot buiten het geheugen van zijn hoop.
  • Extra procesisolatie: arbeider kan zijn geconfigureerd metsecmp-bpf om beschikbare systeemaanroepen te filteren.

Het model is niet zonder risico's: kwetsbaarheden in de V8-parser kunnen dat wel zijn theoretisch ontsnapping uit het isolaat mogelijk maken. Cloudflare heeft een bugbounty-programma toegewijde en regelmatig bijgewerkte V8. Voor werklasten met een hoog beveiligingsniveau biedt Cloudflare uitkomst nog strengere isolatiemethoden.

Beperkingen van het isolatiemodel

Het ontbreken van een koude start heeft een prijs: het geïsoleerde model legt beperkingen op aan containers ze hebben niet:

Beperken Cloudflare-werknemers (gratis/betaald) AWS Lambda
CPU-tijd per verzoek 10 ms (gratis) / 30s (betaald, met pauzes) 15 minuten
Maximaal geheugen 128 MB 10 GB
Bundelgrootte 10 MB (gecomprimeerd) / 1 MB (gratis) 250 MB uitgepakt
Directe TCP Beperkt (Workers TCP Socket API) Volledige toegang
Bestandssysteem Niet beschikbaar /tmp (512MB)
Runtime-talen JavaScript/TypeScript/WASM Elk
Node.js-compatibiliteit Gedeeltelijk (nodejs_compat vlag) Compleet

De limiet van CPU-tijd is het belangrijkste om te begrijpen: het is geen time-out van de wandklok. Werknemers meten feitelijk de CPU-tijd geconsumeerd. Een Worker kan veel asynchrone I/O-aanroepen (fetch, KV) doen dan niet verbruiken CPU-tijd. De limiet is alleen van toepassing op het uitvoeren van JavaScript-code.

Benchmark: Werknemers versus Lambda versus Lambda@Edge

Hier is een vergelijking op basis van openbare benchmarks en officiële documentatie (2025):

Metrisch Cloudflare-werknemers AWS Lambda (Node.js) Lambda@Edge Vercel Edge-functies
Koude start P50 < 1 ms ~200ms ~100ms < 5 ms
Koude start P99 < 5 ms ~1500ms ~500ms < 50 ms
Mondiale PoP's 300+ ~30 regio's ~450 (maar beperkt) ~100+
Wereldwijde gemiddelde latentie ~10ms (aan de rand) ~50-200 ms (regionaal) ~25-100 ms ~30ms
Gratis niveaus 100K verzoek/dag 1M vereist/maand 1M vereiste/maand (gedeelde Lambda) 100 GB uur/maand

Een minimale werker: het uitvoeringsmodel begrijpen

Om het uitgelegde concreet te maken, volgt hier de eenvoudigst mogelijke code toont de levenscyclus van een werknemer:

// worker.ts - formato ES Module (obbligatorio con isolates moderni)

// FASE 1: Module evaluation (eseguita UNA VOLTA, serializzata nello snapshot)
console.log('Questo viene eseguito solo al deploy, non per ogni richiesta');

const CONFIG = {
  version: '1.0',
  region: 'auto',
};

// FASE 2: Export del handler - il punto di ingresso per ogni richiesta
export default {
  // Chiamato per ogni HTTP request
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // request: la richiesta HTTP
    // env: i binding (KV, R2, D1, secrets, variables)
    // ctx: context per waitUntil() - operazioni post-risposta

    const url = new URL(request.url);
    const path = url.pathname;

    // ctx.waitUntil() permette operazioni asincrone dopo la risposta
    ctx.waitUntil(logRequest(request, env));

    if (path === '/health') {
      return new Response(JSON.stringify({
        status: 'ok',
        config: CONFIG,
        timestamp: Date.now(),
      }), {
        headers: { 'Content-Type': 'application/json' },
      });
    }

    return new Response('Not Found', { status: 404 });
  },
};

async function logRequest(request: Request, env: Env): Promise<void> {
  // Questo viene eseguito dopo che la risposta e gia stata inviata
  // Non blocca il client
  await env.ANALYTICS_KV.put(
    `log:${Date.now()}`,
    JSON.stringify({
      url: request.url,
      method: request.method,
      cf: request.cf, // Informazioni Cloudflare: paese, ASN, datacenter, etc.
    })
  );
}

// Interfaccia per i binding definiti in wrangler.toml
interface Env {
  ANALYTICS_KV: KVNamespace;
  API_KEY: string; // secret
}

Het gelijktijdigheidsmodel: één isolaat, veel verzoeken

Een subtiel maar belangrijk aspect: in tegenstelling tot Lambda, waar elke aanroep de eigen geïsoleerde omgeving, in Workers hetzelfde isolaat aankan meerdere concurrerende verzoeken. Dit is mogelijk omdat JavaScript is single-threaded met gebeurtenislussen, dus er is geen echte gelijktijdigheid bij gedeelde status, maar I/O-gebonden verzoeken kunnen worden gemultiplext.

// ATTENZIONE: stato globale condiviso tra richieste
// In Lambda questo non sarebbe un problema (ogni invocazione ha il suo processo)
// In Workers, piu richieste possono condividere lo stesso isolate

// SBAGLIATO: questo contatore e condiviso tra tutte le richieste
let requestCount = 0;

export default {
  async fetch(request: Request): Promise<Response> {
    requestCount++; // Race condition! Non fare questo.
    return new Response(`Request #${requestCount}`);
  },
};

// CORRETTO: usa ctx.waitUntil() per operazioni post-risposta
// e storage esterno (KV) per contatori condivisi
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const count = parseInt(await env.COUNTERS.get('requests') ?? '0') + 1;
    ctx.waitUntil(env.COUNTERS.put('requests', String(count)));
    return new Response(`Requests: ${count}`);
  },
};

Mondiale status van werknemers: aandacht

In tegenstelling tot Lambda (waar elke aanroep geïsoleerd is), is in Workers de staat Globaal JavaScript kan worden gedeeld tussen intern gelijktijdige verzoeken van hetzelfde geïsoleerd. Gebruik altijd externe opslag (KV, D1, R2) voor gegevens die nodig zijn blijven bestaan of gedeeld worden. Onveranderlijke globale variabelen (configuratie, router) ze zijn veilig; veranderlijke variabelen zijn gevaarlijk.

Workerd: de open source-runtime

In september 2022 maakte Cloudflare het open-source arbeider (github.com/cloudflare/workerd), de runtime die Workers aandrijft. Dit had hij belangrijke gevolgen:

  • Deno implementeren adopteerde V8-isolaten met vergelijkbare architectuur
  • Miniflare (de lokale simulator) gebruikt workerd intern sinds v3
  • U kunt workerd on-premise uitvoeren voor privéomgevingen
  • De community kan bijdragen aan de runtime en de beveiligingscode inspecteren
# Architettura workerd (semplificata)

┌─────────────────────────────────────────────────┐
│                  workerd process                │
│                                                 │
│  ┌──────────────┐  ┌──────────────┐            │
│  │  Isolate #1  │  │  Isolate #2  │   ...      │
│  │  (Worker A)  │  │  (Worker B)  │            │
│  │              │  │              │            │
│  │  V8 Heap A   │  │  V8 Heap B   │            │
│  │  (isolato)   │  │  (isolato)   │            │
│  └──────┬───────┘  └──────┬───────┘            │
│         │                 │                    │
│  ┌──────▼─────────────────▼───────┐            │
│  │        I/O Subsystem           │            │
│  │  (fetch, KV, R2, D1, DO, AI)  │            │
│  └───────────────────────────────┘            │
│                                                 │
│  ┌─────────────────────────────────────────┐   │
│  │        Network Layer                    │   │
│  │  (Cloudflare anycast, TLS, HTTP/3)     │   │
│  └─────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘

Wanneer mag u GEEN werknemers gebruiken?

V8-isolaten zijn niet het antwoord op alles. Er zijn scenario's waarbij Lambda of container blijft de juiste keuze:

  • Lange CPU-intensieve berekeningen: ML-training, videoweergave, audiocodering. De limiet van 30 seconden CPU-tijd en 128 MB RAM is onbetaalbaar.
  • Niet-JavaScript-code: Workers ondersteunt WebAssembly, maar niet alles talen compileren goed in WASM. Native Python, Java en Ruby vereisen Lambda.
  • Compleet Node.js-ecosysteem: Veel npm-bibliotheken gebruiken native Node.js-API's (fs, geavanceerde crypto, buffermanip) niet beschikbaar in Workers.
  • Complex staatsbeheer: Als u WebSocket-sessies nodig heeft duurzame objecten met veel status, denk eens aan Duurzame Objecten (zie artikel 4 in de serie).
  • Database voor permanente verbindingen: Werknemers onderhouden geen verbindingen Persistent TCP tussen verzoeken. Gebruik Hyperdrive voor pooling met traditionele databases.

Conclusies en volgende stappen

De V8-isolaten vertegenwoordigen een architecturale paradigmaverschuiving, niet alleen een optimalisatie: ze verplaatsen de isolatiegrens van het kernelniveau naar het runtimeniveau, wat resulteert in tijden van Opstarten in minder dan een milliseconde met voldoende beveiliging voor edge computing met meerdere tenants. De kosten het is een beperktere uitvoeringsomgeving dan traditionele containers.

Voor de meeste RESTful API's, authenticatie-middleware, transformatie van verzoeken en aanpassing van de inhoud, de beperkingen van werknemers zijn meer dan alleen aanvaardbaar, en de winst in latentie is globaal en meetbaar.

Volgende artikelen in de serie

  • Artikel 2: Je eerste Cloudflare-werker: Fetch Handler, Wrangler en Deploy: van concept tot praktijk, met een werkende medewerker in de productie.
  • Artikel 3: Edge-persistentie — werknemers KV, R2 en D1 SQLite: wanneer en hoe elke opslaglaag die beschikbaar is in Workers moet worden gebruikt.
  • Artikel 4: Duurzame objecten – Sterk consistente staat e WebSocket: de krachtigste primitief voor stateful applicaties aan de edge.