Skip to content
ZERONE
Torna agli insights
Sistemi distribuiti2026-07-06 · 8 min di lettura

Quattro strati di protezione dei costi: come il nostro auto-scaler non fa esplodere la fattura

Un cliente carica una scena rotta, l'orchestrator avvia un pod, il pod finisce in OOM infinito, tenta tre retry, ogni pod ci costa 0,80 dollari al minuto. Senza protezione, questo genera in un'ora una fattura a tre cifre. Quattro strati indipendenti devono fallire prima che accada.

In un setup di cloud rendering l'errore più costoso non è il render lento — è il render che gira all'infinito. Un GPU pod a US$0.80/minuto diventa $48 in 60 minuti, $576 in 12 ore, $2.300 in un weekend. Se il software può crashare e l'orchestrator „lo fa semplicemente ripartire", non è auto-recovery — è una fattura che fa sì che il cliente non torni più.

Il nostro backend cinema-orch ha quattro strati indipendenti di cost protection. Dopo lo Sprint 3.5 (2026-06-15) sono tutti e quattro live. Ogni strato fallisce diversamente dagli altri — così devono fallire almeno due prima di vedere fatture in overshoot.

Strato 1: estimate ex-ante + client-confirm

Prima che qualsiasi pod venga spawnato, l'orchestrator calcola un estimate dei costi basato su:

  • Sample count × tile count × frame count × tempo stimato per frame sulla GPU scelta
  • Baseline da job storici della stessa signature di scena (hash da versione Blender + render settings)
  • Buffer del 20 % per l'ignoto

L'estimate viene mostrato al cliente prima dell'avvio. Il cliente deve cliccare attivamente „Conferma". Se l'estimate è sopra il budget del cliente (ogni user fissa un hard-cap nel suo account), il job viene automaticamente respinto — non messo in coda, non confermato-con-warning, ma rejected.

Effetto: circa il 70 % di tutti i job teoricamente costosi non entra mai nell'orchestrator, perché il cliente vede prima che diventerà troppo caro e ottimizza la scena.

Strato 2: budget cap per-job con ticker live

Ogni job ha un campo `max_budget_cents`, impostato dall'utente (tipicamente estimate + 20 %). L'orchestrator fa girare un ticker in Postgres — ogni minuto il costo attualmente accumulato viene calcolato e confrontato con `max_budget_cents`.

Se il job raggiunge il cap, il pod viene terminato in modo hard. L'utente riceve una notification, può alzare manualmente il cap o prendere l'output parziale.

Effetto: un runaway render può superare al massimo il cap del ~5–10 % (finché non scatta il prossimo tick del ticker). Su un job da $50 sono $2.50 di overshoot invece di $2.300.

Strato 3: pod timeout a livello systemd

Ogni GPU pod ha un limite temporale hard, impostato nell'init del pod — analogo a un ActiveDeadline di Kubernetes. Per una standard render session: 4 ore. Alla scadenza: il pod muore, l'orchestrator vede l'abort e marca il job come `budget_exceeded`.

Perché è necessario nonostante esista lo strato 2: l'orchestrator stesso potrebbe crashare (partizionamento di rete, downtime di Postgres, OOM del processo). Il pod timeout è la garanzia di fallback — è configurato sul pod stesso e non ha bisogno di trigger esterni.

Effetto: anche con un orchestrator completamente muto, un singolo pod costa al massimo il limite delle 4 ore, dopodiché si termina da solo.

Strato 4: spending alert lato provider

Runpod (il nostro primary GPU provider) ha propri budget alert per account. A $50/giorno di spend arriva una email a [email protected]; a $200/giorno un Slack alert nel nostro canale Ops. Entrambi impostati in modo conservativo.

Se gli altri tre strati falliscono tutti, al più tardi questo alert ci tira dentro manualmente. Finora (4 settimane di beta) non è mai scattato, perché gli strati 1–3 catturano tutto.

Perché quattro

Un singolo protection layer non è protezione — è un single point of failure. Se ci affidiamo al „cliente conferma l'estimate" e la UI ha un bug che mostra l'estimate troppo basso, siamo fritti. Quattro strati indipendenti significa: un bug in un qualsiasi software (la nostra UI, il nostro orchestrator, systemd, il portale Runpod) non fa passare una fattura inattesa — la catturano gli altri tre.

Il trucco è l'indipendenza. Lo strato 1 vive nel frontend (Next.js). Lo strato 2 nell'orchestrator (Python/asyncpg). Lo strato 3 sul pod (systemd). Lo strato 4 dal provider (API di Runpod). Se un software dovesse crashare, è improbabile che gli altri tre falliscano allo stesso modo nello stesso momento.

Cosa abbiamo imparato

  • La precisione dell'estimate è la leva più importante per lo strato 1. Se l'estimate è 30 % fuori bersaglio, il buffer del cap si esaurisce prima che il job finisca. Abbiamo investito molto tempo nella calibrazione sample count × tempo/frame.
  • L'intervallo del ticker nello strato 2 è un trade-off tra overshoot e load sul DB. 60 secondi è il nostro compromesso — 30 secondi riduce l'overshoot, ma raddoppia il load DB per job attivo.
  • Il pod timeout nello strato 3 non deve essere troppo corto. In una versione iniziale avevamo 60 minuti — ha ucciso un legittimo job 4K che avrebbe avuto bisogno di 90 minuti. Adesso 4 ore di default, configurabile per job.

Se stai costruendo un sistema di auto-scaling con rischio di fatturazione GPU esterna: non partire con un singolo cap. Costruiscine tre indipendenti, più un provider alert. Lo sforzo extra è piccolo, il delta di rischio è enorme.

Documentiamo questa architettura pubblicamente perché pensiamo che il cloud rendering debba essere onesto sui propri rischi. Se vuoi che io guardi il pattern per il tuo sistema: [email protected]ZER0ONE Studio offre questo tipo di consulenza distributed-systems come engineering service.

Stesso fuoco anche da te?

Probabilmente abbiamo già visto qualcosa di simile. Parliamone.

Iniziamo a parlarne