Il limite di 10 ms della CPU sul piano Workers gratuito spaventa i lettori alle prime armi. Dieci millisecondi sembrano ridicolmente pochi per qualcosa di utile. La conseguenza è che molti team passano immediatamente al piano pagato per i 30 secondi di CPU, senza capire che il modello di misurazione è fondamentalmente diverso da un server tradizionale e che la maggior parte dei lavoratori ha CPU rimasta anche nei 10 ms gratuiti. Il limite di memoria di 128 MB, invece, è sistematicamente sottostimato ed è responsabile di un'intera categoria di guasti silenziosi in produzione.
Come funziona effettivamente il timer della CPU
Il runtime Workers misura il "tempo CPU": il tempo in cui il thread JavaScript esegue attivamente il codice. Il timer si ferma durante qualsiasi operazione I/O asincrona: await fetch(), await env.KV.get(), await db.query(), await env.BUCKET.get(). Durante queste attese, l'isolamento è inattivo e il timer non avanza.
L'effetto pratico è fantastico. Un Worker che effettua cinque chiamate sequenziali fetch() ad API esterne, ciascuna impiegando 100 ms di latenza di rete, ha un tempo di clock di 500 ms ma utilizza forse 6 ms di CPU: solo la serializzazione delle intestazioni, l'analisi del JSON di risposta e la logica di business tra le chiamate. Per la maggior parte dei lavoratori che sono essenzialmente orchestratori di I/O, il limite di 10 ms è generoso.
Ciò che utilizza realmente la CPU sono operazioni sincrono intense: regex applicate a stringhe di grandi dimensioni, oggetti con gerarchia profonda o array di grandi dimensioni, operazioni crittografiche anche quando l'API è asincrona (il lavoro di hashing avviene sulla CPU durante l'esecuzione) e codifica/decodifica base64 su binari di grandi dimensioni. Un lavoratore che riceve un payload JSON da 500 KB e esegue JSON.parse() su di esso impiegherà tempo CPU misurabile su questa operazione: l'analisi è sincrona.
Nel piano a pagamento, il limite arriva fino a 30 secondi di tempo CPU, il che è sufficiente per casi di utilizzo intensivo dal punto di vista computazionale: compressione, generazione di immagini, inferenza di piccoli modelli. Ma anche a pagamento, le operazioni della CPU più lunghe di pochi secondi sono un sintomo di una progettazione scadente per l'edge: i lavoratori sono stati ottimizzati per una bassa latenza, non per un'elaborazione pesante.
Il timer della CPU non è ciò che uccide i lavoratori in produzione
Ciò che realmente abbatte i lavoratori nella produzione senza un chiaro preavviso è la memoria. Il limite di 128 MB sembra ragionevole finché non si capisce cosa conta: lo script non compresso sull'heap V8, tutte le chiusure dei moduli che esistono nell'ambito globale dall'inizio dell'invocazione, più tutto ciò che viene allocato durante l'elaborazione della richiesta in corso.
Lo script stesso può essere più impegnativo di quanto pensi. Un Worker compresso da 500KB può occupare 3-4MB dopo essere stato decompresso e analizzato da V8. Le dipendenze importate nell'ambito del modulo (librerie di convalida, parser, SDK) rimangono in memoria per tutta la durata dell'isolamento, anche se non vengono utilizzate nella richiesta corrente. L'ambito globale è condiviso tra le richieste che lo stesso isolamento elabora in sequenza nello stesso PoP.
L'allocazione che più spesso fa scattare il limite è response.arrayBuffer() o request.arrayBuffer(). La chiamata a questo metodo su una risposta da 40 MB alloca immediatamente 40 MB di heap. Se il Worker crea quindi più strutture dati da questo buffer (oggetti analizzati, copie trasformate), l'utilizzo della memoria può superare i 128 MB prima del termine dell'elaborazione. A questo punto il runtime termina l'isolamento, il client riceve un errore 1101 e non esiste alcuna traccia dello stack, solo un errore di runtime segnalato come eccezione generica nella coda del wrangler.
Lo streaming come strategia di sopravvivenza della memoria
La soluzione al problema della memoria con carichi utili di grandi dimensioni è non materializzare mai l'intero contenuto come buffer. Invece di response.arrayBuffer(), usa response.body — che è un ReadableStream — ed elabora i dati in blocchi con TransformStream.
L'API Workers streams segue la specifica WHATWG Streams, la stessa disponibile nei browser moderni. Un TransformStream ha un writable e un readable: colleghi il readable della risposta upstream al writable della trasformazione e colleghi il readable della trasformazione alla risposta che invii al client. I blocchi scorrono attraverso la pipeline senza mai avere interi numeri interi in memoria contemporaneamente.
Questa architettura ha una conseguenza importante: non è più possibile leggere l'intero contenuto per prendere decisioni che dipendono dall'intero file prima di iniziare a rispondere. Per i casi in cui è necessario un accesso casuale, ad esempio l'elaborazione di un CSV che richiede un ordinamento globale, Workers non è il posto giusto. Per le trasformazioni che operano pezzo per pezzo (ricompressione, sostituzione del testo, filtraggio delle righe) la pipeline del flusso lo risolve senza pressione sulla memoria.
Per i caricamenti diretti a R2, l'associazione accetta direttamente un ReadableStream:
await env.BUCKET.put(key, request.body, { httpMetadata }) — senza bufferizzare nulla. Il flusso del corpo della richiesta va direttamente a R2 in blocchi.
Operazioni sorprendenti per l'utilizzo della CPU
La codifica e la decodifica Base64 sono operazioni ad uso intensivo della CPU proporzionali alla dimensione dei dati e il risultato occupa il 33% di memoria in più. Se stai trasportando file binari, vale la pena chiedersi se è necessaria la codifica; Molti usi di base64 sono limitazioni HTTP legacy che non si applicano più ai tipi fetch e binari nativi.
Le operazioni di hashing tramite l'API Web Crypto sono asincrone nella firma — await crypto.subtle.digest('SHA-256', data) — ma l'hashing consuma CPU proporzionale alla dimensione dei dati. Un lavoratore che esegue HMAC-SHA256 su ogni richiesta di verifica di un webhook sta spendendo una CPU reale su di esso, irrilevante per carichi utili piccoli ma significativa al di sopra di poche centinaia di KB.
Regex su stringhe lunghe può essere sorprendentemente costoso. I modelli con backtracking catastrofico (più quantificatori annidati sullo stesso set di caratteri) possono triplicare il tempo della CPU su stringhe di pochi kilobyte. Misura con stringhe realistiche, non input da 10 byte che funzionano nel test.
Cosa cercare per rilevare la pressione sulle risorse
wrangler tail restituisce cpuTime su ogni evento: il tempo della CPU misurato in millisecondi per quell'invocazione. La registrazione strutturata di questo valore ci consente di rilevare regressioni: un deploy che aumenta CPU p99 da 3ms a 12ms significa che è stata introdotta qualche nuova operazione sincrona.
L'utilizzo della memoria non viene esposto direttamente dall'invocazione in tail. Il modo per rilevare la pressione della memoria prima di superare il limite è osservare il comportamento dell'isolato: se il runtime inizia a creare nuovi isolati più frequentemente del normale per lo stesso PoP (visibile come un aumento degli avvii a freddo) potrebbe essere un segno che gli isolati vengono eliminati prima dal Garbage Collector o a causa della pressione della memoria.
La possibilità di riutilizzare un isolato tra richieste nello stesso PoP è un'ottimizzazione importante: il costo dell'avvio a freddo (decompressione dello script, inizializzazione V8, esecuzione del codice del modulo di livello superiore) avviene una volta e le richieste successive riutilizzano l'isolato già riscaldato. Le chiusure dei moduli che risiedono in memoria tra le richieste sono il meccanismo di memorizzazione nella cache più veloce disponibile in Workers: più veloce di KV, più veloce di qualsiasi sottorichiesta.
Leggi anche
- Operatori Cloudflare in produzione: cosa cambia dopo hello world
- D1 nella produzione: prestazioni, limiti e cosa non si adatta da solo
- Operatori Cloudflare: Guida pratica all'Edge Computing Serverless
- Oggetti durevoli in produzione: come sarà la bolletta e i limiti che sorprendono
- [WebAssembly al limite: perché è importante iniziare in modo rapido e isolato28
- Lavoratori + D1 + KV + R2: comporre associazioni nello stesso servizio
