Cloudflare Workers
Produção
Observabilidade
Performance
Serverless

Cloudflare Workers in produzione: cosa cambia dopo hello world

L'ambiente locale di Workers è ingannevolmente confortevole: la produzione ha un impatto diverso su log, memoria e limiti di sottorichieste.

Cloudflare Workers in produzione: cosa cambia dopo hello world

Sviluppare localmente con Cloudflare Workers è una delle esperienze più fluide che il serverless possa offrire oggi. Il wrangler dev sale in pochi secondi, i log vengono visualizzati nel terminale e tutto sembra funzionare esattamente come previsto. Il problema è che questa comodità maschera differenze fondamentali che appaiono solo quando il codice raggiunge veramente il limite. Molti team scoprono tardi che console.log in produzione non porta da nessuna parte a meno che non ci sia qualcuno che esegue attivamente wrangler tail, che bufferizzare una risposta da 50MB uccide l'isolamento silenziosamente e che quel gestore che fa 15 query in D1 più 10 letture in KV è una bomba a orologeria contro il limite delle sottorichieste.

Cosa succede ai tuoi log in produzione

Localmente, wrangler dev viene visualizzato ogni console.log sul terminale in tempo reale. In produzione, gli isolati V8 di Cloudflare non dispongono di un processo persistente per acquisire questo output: ogni invocazione viene eseguita in isolamento, viene eseguita e scompare. I log sono accessibili solo tramite wrangler tail, che apre una sessione di streaming con i lavoratori in produzione e inoltra la richiesta di metadati, output console.log, eccezioni non rilevate e durata della CPU.

Il punto critico: questa sessione non persiste. Se non c'è nessuno che segue quando si verifica un errore, il registro viene perso. Per quanto riguarda la conservazione, la soluzione è Logpush, che esporta i log in R2, S3, Datadog o Splunk a 0,05 dollari per milione di righe per R2. Ma Logpush funziona con i campi strutturati emessi dal runtime Worker, non con il testo libero. La conseguenza pratica è che i log utili in produzione devono essere strutturati fin dall'inizio: JSON con campi come requestId, duration, statusCode, error — campi che sia wrangler tail possono filtrare sia Logpush può esportare fedelmente.

Il modello che funziona è creare un wrapper di registrazione minimo all'inizio del progetto, prima che ne abbia bisogno. Qualcosa che serializza su JSON e scrive in console.log, con un campo level che differenzia info da error. Quando entra Logpush, questi campi sono disponibili per filtri e avvisi sul target.

Memoria: 128 MB sono più piccoli di quanto sembri

Il limite di 128 MB per isolato include lo script non compresso nella V8, tutte le chiusure dei moduli e l'intero heap dell'invocazione corrente. Non sono 128 MB per i "tuoi dati": sono 128 MB per tutto, incluso il runtime stesso.

L'errore più comune è chiamare response.arrayBuffer() nelle risposte grandi. Un file da 30 MB scaricato da un altro servizio per essere elaborato e inoltrato occupa immediatamente 30 MB di heap. Se l'elaborazione crea più allocazioni intermedie, l'isolato supera i 128 MB e viene interrotto: nessuna eccezione rilevabile, nessuna risposta al client, solo un errore 1101 all'esterno.

La soluzione è usare response.body come ReadableStream ed elaborare tramite TransformStream. Invece di buffering e trasformazione, crei una pipeline in cui scorrono i blocchi: letti da upstream, trasformati in transito, scritti in risposta senza che siano mai esistiti numeri interi in memoria. Per risposte di dimensioni superiori a 1 MB, il presupposto dovrebbe essere lo streaming; Il buffering dovrebbe essere una scelta esplicita e giustificata, non il percorso predefinito.

Lo stesso ragionamento vale per i caricamenti. Il corpo massimo della richiesta è 100 MB, ma request.arrayBuffer() tenta di allocare tutto in una volta. Per caricamenti di grandi dimensioni, anche l'elaborazione deve essere eseguita nello stream oppure il file deve andare direttamente a R2 tramite put() che accetta un ReadableStream.

Sottorichieste: il limite che appare nel momento peggiore

Ogni fetch(), ogni operazione su KV, ogni interrogazione su D1, ogni lettura su R2 conta come una sottorichiesta. Nel piano a pagamento il limite è 1000 per invocazione. Gratis, 50.

Un gestore che sembra ragionevole (recupera l'utente in D1, legge le sue preferenze in KV, estrae il documento da R2, chiama un'API esterna, salva il risultato in D1) è già a 5 sottorichieste nel percorso felice. Se questo gestore entra in un ciclo perché elabora un elenco di elementi, il contatore aumenta rapidamente. Cinquanta elementi con due operazioni ciascuno sono già vicini a 100. Un bug che effettua N+1 query in D1, cercando ciascun record figlio individualmente invece che con un JOIN, può facilmente superare 1000 prima della fine di una singola richiesta complessa.

L'errore al raggiungimento del limite non è evidente: il Worker riceve un errore di rete sulla sottorichiesta che ha superato il limite, che può essere confuso con instabilità della rete o del servizio a valle. La diagnosi corretta richiede la visualizzazione dei registri delle eccezioni tramite wrangler tail e la correlazione con il modello di chiamata del gestore.

Mitigazione: utilizzare db.batch() in D1 per raggruppare più query in un'unica sottorichiesta. Leggi le configurazioni KV una volta all'inizializzazione del modulo e memorizzale nella cache nell'ambito globale: l'isolato può essere riutilizzato tra richieste nello stesso PoP e la lettura KV avvenuta nella prima invocazione non deve essere ripetuta nelle invocazioni successive finché l'isolato è in vita.

Segreti, ambienti e wrangler.toml che va al repository

Le variabili dichiarate in wrangler.toml sotto [vars] sono testo in chiaro nel file di configurazione, che normalmente va nel repository. Per qualsiasi valore sensibile (chiavi API, token bancari, segreti webhook) l'unica opzione corretta è Workers Secrets, dichiarata come [secrets] in wrangler.toml e archiviata tramite wrangler secret put. Il valore viene crittografato a riposo e inserito in fase di esecuzione come proprietà di env, senza apparire in alcun log di build o artefatto.

Il wrangler.toml di un servizio che va in produzione deve avere ambienti espliciti: [env.staging] e [env.production] con i propri collegamenti, percorsi e segreti separati. Mescolare allestimento e produzione nello stesso set di attacchi è un incidente in attesa di accadere, soprattutto quando D1 e KV dispongono di dati reali in produzione e dati di test in allestimento. La separazione degli ambienti in wrangler.toml consente inoltre alla pipeline CI di eseguire automaticamente la distribuzione in staging e di richiedere l'approvazione manuale per la produzione, senza alcuna logica aggiuntiva nello script di distribuzione.

Come si presenta una configurazione di produzione minima

Un wrangler.toml pronto per la produzione fa riferimento esplicitamente a compatibility_date (in modo da non ricevere modifiche sostanziali dal runtime senza preavviso), definisce [observability] con enabled = true per esporre le metriche di base sul dashboard e separa [env.staging] da [env.production] con percorsi diversi. I segreti sono elencati per nome senza valore: il valore esiste solo in Cloudflare, mai nel repository.

Il gestore principale ha un try/catch al livello più alto che rileva eventuali eccezioni non gestite, le registra come strutturato JSON con console.error e restituisce una risposta HTTP 500 con un requestId tracciabile. Senza questo limite, un'eccezione inaspettata potrebbe restituire al client un 200 con il corpo troncato mentre l'errore reale appare solo nella coda e solo se qualcuno sta guardando.

Leggi anche