1. Perché pensare alla «resilienza» in una ChatGPT App
In una normale web app l’utente vede almeno l’URL, lo spinner del browser, può ricaricare la pagina. In ChatGPT l’utente vede un unico schermo: la chat e la tua App. Se qualcosa rallenta, non distingue chi è colpevole — OpenAI, il tuo Gateway, il sistema di pagamenti o il microservizio di analytics del vicino. Per lui è tutto “ChatGPT + la tua App”.
Quando un tool-call resta in sospeso per 30–60 secondi, il modello aspetta, aspetta… e nel migliore dei casi si scusa per il ritardo. Nel peggiore — allucina una risposta al posto dei dati dal tuo backend. Quindi la resilienza non riguarda solo SRE e uptime, ma anche la qualità della risposta, il tono del modello e le metriche nello Store.
Nell’ecosistema di una ChatGPT App abbiamo diversi circuiti indipendenti:
- ChatGPT ↔ MCP Gateway.
- Gateway ↔ i tuoi servizi backend/REST (Gift REST API, Commerce REST API, Analytics Service ecc.).
- I tuoi servizi ↔ API esterne (LLM, pagamenti, cataloghi).
- Webhook in ingresso (ACP, Stripe, qualsiasi integrazione) ↔ i tuoi handler.
Il problema è che un guasto in un punto può innescare una cascata: il Gateway aspetta onestamente un servizio bloccato, i worker si saturano, le connessioni finiscono, i client iniziano a fare retry e in pochi minuti hai il classico scenario “tutto brucia e affonda insieme”. Proprio da questo ci proteggono i quattro pattern di cui parliamo oggi:
- Timeouts — non aspettiamo mai all’infinito.
- Circuit breaker — non prendiamo a testate una porta chiusa.
- Bulkheads — costruiamo “compartimenti” e non facciamo affondare tutta la nave.
- Protezione dalle tempeste di webhook — accettiamo che i webhook arrivano con duplicati, picchi e retry e ci prepariamo.
2. Timeouts: non aspettiamo all’infinito
Che cos’è un timeout e perché senza di esso va tutto male
Un timeout è il tempo massimo che il tuo codice è disposto ad aspettare la risposta da una dipendenza: database, server MCP, API HTTP esterna, modello. Se la risposta non arriva entro il tempo impostato — consideriamo la chiamata non riuscita, liberiamo le risorse e restituiamo un errore comprensibile o un fallback.
Senza timeout le richieste possono:
- restare appese in attesa per sempre,
- occupare connessioni e pool di thread,
- bloccare le richieste successive,
- provocare guasti a cascata.
Il pattern è semplice: «meglio un errore prevedibile dopo 3–5 secondi che un silenzio inspiegabile per 5 minuti».
È importante ricordare che abbiamo timeout su più livelli:
- a livello di proxy/bilanciatore (Cloudflare, Nginx),
- a livello di MCP Gateway (client HTTP verso i microservizi),
- all’interno dei servizi stessi (chiamate al DB, API esterne, LLM).
Per ChatGPT in generale ha senso puntare a un tempo totale del tool-call nell’intervallo 5–10 secondi per operazioni normali e massimo 20–30 secondi per quelle più pesanti. Oltre questi valori — quasi garantito un cattivo UX.
fetchWithTimeout semplice in TypeScript
Partiamo dalla pratica. Nel GiftGenius MCP Gateway abbiamo un client HTTP di supporto che parla con il motore di selezione dei regali, con il servizio commerce e con l’analytics. Wrappiamo il fetch standard in una funzione con timeout:
// src/gateway/httpClient.ts
export async function fetchWithTimeout(
url: string,
opts: RequestInit & { timeoutMs?: number } = {}
) {
const { timeoutMs = 5000, ...rest } = opts;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...rest, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
Ora, nel codice del Gateway non facciamo mai un fetch “nudo”, ma solo tramite questo helper:
// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";
export async function callGiftService(path: string) {
const res = await fetchWithTimeout(
process.env.GIFT_SERVICE_URL + path,
{ timeoutMs: 4000 }
);
if (!res.ok) {
throw new Error(`gift_service_${res.status}`);
}
return res.json();
}
Questo approccio garantisce che anche se il servizio gift si blocca, dopo 4 secondi interrompiamo la connessione e possiamo restituire un errore MCP a ChatGPT, invece di tenere la connessione fino alla fine.
Dove impostare i timeout in GiftGenius
Nel nostro esempio GiftGenius:
- A livello di Gateway: timeout sulle chiamate a Gift REST API, Commerce REST API, Analytics Service / REST API.
- All’interno di questi servizi: timeout sulle chiamate al DB, ad ACP/sistemi di pagamento, a API raccomandative esterne.
- All’ingresso del Gateway: timeout generale della richiesta da ChatGPT, affinché il tool-call non si trasformi in uno “spinner infinito”.
È importante che il tempo di attesa al livello superiore sia leggermente maggiore rispetto a quello interno. Ad esempio, se il Gateway aspetta il backend 5 secondi e il backend aspetta il DB 3 secondi, abbiamo margine per l’elaborazione e la serializzazione del risultato.
Come spiegare i timeout al modello di ChatGPT
Per ChatGPT è essenziale restituire errori semantici, non interrompere silenziosamente le connessioni. Invece di un generico 500 è meglio restituire un errore MCP strutturato che il modello possa riferire all’utente: «Il servizio di selezione dei regali è temporaneamente sovraccarico, prova di nuovo tra poco» e così via.
Questo significa che nel Gateway, in caso di timeout, devi:
- Catturare AbortError o il nostro timeout_….
- Formare una risposta MCP con un codice significativo e una breve descrizione.
- Dare al modello la possibilità di decidere come spiegarlo alla persona.
I timeout risolvono il problema delle richieste bloccate, ma se una dipendenza ha iniziato a fallire in massa, non ci salvano da una valanga di tentativi ugualmente fallimentari. Qui serve il livello successivo di protezione — il circuit breaker.
3. Circuit breaker: l’“interruttore” contro i servizi morenti
Intuizione: perché un solo timeout non basta
Abbiamo già imparato a limitare il tempo di attesa delle singole chiamate con i timeout. Il timeout protegge una singola chiamata. Ma se la dipendenza è “morta” sul serio (per esempio, il servizio commerce va in OOM — Out Of Memory — a ogni richiesta), continueremo a chiamarlo, ogni volta aspettando 3–5 secondi, catturando l’errore, caricando rete e CPU e ricominciando da capo.
Il circuit breaker aggiunge memoria: traccia errori e timeout e, quando diventano troppi, smette proprio di inviare richieste a quel servizio. Al loro posto restituisce un rifiuto rapido o un fallback. Dopo un po’ riprova con cautela in stato half-open.
Stati classici dell’interruttore:
- Closed — tutto ok, le richieste passano.
- Open — il servizio è considerato “morto”, le richieste non passano, errore immediato.
- Half-open — proviamo un numero limitato di richieste; se hanno successo — torniamo a closed, se falliscono di nuovo — torniamo a open.
Schema semplice di circuit breaker
Piccolo diagramma:
stateDiagram-v2
[*] --> Closed
Closed --> Open: troppi errori
Open --> HalfOpen: cooldown scaduto
HalfOpen --> Closed: alcuni successi consecutivi
HalfOpen --> Open: di nuovo errori
Open --> Open: rifiuto rapido
Mini-implementazione di circuit breaker in TypeScript
In produzione si usano di solito librerie pronte (per Node.js esistono, ad esempio, opossum o soluzioni leggere self-made), ma per capirne la meccanica basta una classe compatta.
Esempio di breaker estremamente semplificato attorno alla chiamata del modulo commerce:
// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";
export class CircuitBreaker {
private state: State = "closed";
private failureCount = 0;
private nextAttemptAt = 0;
constructor(
private readonly failureThreshold = 5,
private readonly cooldownMs = 30_000
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
const now = Date.now();
if (this.state === "open") {
if (now < this.nextAttemptAt) {
throw new Error("circuit_open");
}
this.state = "half-open";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "closed";
}
private onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
this.nextAttemptAt = Date.now() + this.cooldownMs;
}
}
}
E utilizzo nel client del servizio commerce:
// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);
export async function callCommerce(path: string) {
return commerceBreaker.call(async () => {
const res = await fetchWithTimeout(
process.env.COMMERCE_URL + path,
{ timeoutMs: 3000 }
);
if (!res.ok) throw new Error(`commerce_${res.status}`);
return res.json();
});
}
Qui, quando commerce inizia a rispondere con errori in massa o non fa in tempo prima del timeout, dopo alcuni fallimenti il breaker passa a open. In questo stato, per la durata di cooldownMs non proviamo nemmeno a chiamare il servizio e restituiamo subito l’errore circuit_open.
Cosa dovrebbe vedere ChatGPT quando il breaker “stacca” il servizio
Dal punto di vista di ChatGPT è meglio se:
- Rispondi rapidamente con un errore MCP “commerce_unavailable” o “gift_service_overloaded”.
- Aggiungi una descrizione chiara: «Il servizio di pagamento è temporaneamente non disponibile, proviamo più tardi».
- Non nascondi l’errore dietro retry infiniti.
Questo è proprio il caso in cui «un rifiuto onesto e veloce» è meglio di un lungo blocco. Specialmente nel checkout: l’utente accetta più facilmente un messaggio onesto che guardare lo spinner per 40 secondi e ottenere «qualcosa è andato storto».
Timeout e breaker ci proteggono da dipendenze “cattive” o down, ma non risolvono il problema in cui un tipo di carico consuma tutte le risorse e inizia a soffocare il resto del sistema. Serve un altro strato — i bulkheads.
4. Bulkheads: isolare i «compartimenti» per non far affondare tutta la nave
Analogia con una nave
Il pattern bulkhead prende il nome dalle paratie delle navi: se c’è una falla in un compartimento, l’acqua non si diffonde in tutta la nave. In architettura significa: separare le risorse tra diverse linee di lavoro, così che un servizio sovraccarico non consumi tutto — CPU, connessioni, pool — e non abbatta i percorsi critici.
Nei microservizi si fa di solito con:
- pool separati di connessioni HTTP,
- pool separati di thread/worker,
- code/topic separati,
- persino cluster DB separati per operazioni critiche.
L’idea è che se il servizio di raccomandazione dei regali inizia a rallentare e a incepparsi, esaurirà solo le sue risorse, ma non romperà il checkout e l’autorizzazione.
Bulkheads nel mondo Node.js e MCP Gateway
In Node.js non abbiamo thread nel senso classico (c’è l’event loop e i worker), ma possiamo limitare il numero di attività parallele per ogni direzione.
Esempio: nel Gateway ci sono tre dipendenze esterne:
- Servizio Gift (selezione dei regali, chiamate LLM pesanti).
- Servizio Commerce (checkout, ACP).
- Servizio Analytics (logging degli eventi).
Possiamo introdurre limiti semplici alle richieste contemporanee verso ciascuno di essi.
Per esempio, un piccolo “semaforo” per limitare la parallelizzazione:
// src/gateway/bulkhead.ts
export class Bulkhead {
private active = 0;
private queue: (() => void)[] = [];
constructor(private readonly maxConcurrent: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.active >= this.maxConcurrent) {
await new Promise<void>((resolve) => this.queue.push(resolve));
}
this.active++;
try {
return await fn();
} finally {
this.active--;
const next = this.queue.shift();
if (next) next();
}
}
}
E utilizzo per i servizi:
// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";
const giftBulkhead = new Bulkhead(10); // fino a 10 in parallelo
const commerceBulkhead = new Bulkhead(3); // checkout fortemente limitato
const analyticsBulkhead = new Bulkhead(50); // alta concorrenza consentita
export async function callGiftWithBulkhead(fn: () => Promise<any>) {
return giftBulkhead.run(fn);
}
export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
return commerceBulkhead.run(fn);
}
Così, anche se GPT decidesse di chiedere in massa «fammi 30 selezioni di regali complesse», verrebbero eseguite al massimo 10 in parallelo, e il checkout potrebbe continuare a funzionare usando il proprio limite separato.
GiftGenius: quali compartimenti vogliamo
In GiftGenius ha senso creare compartimenti separati per:
- Selezione dei regali (LLM pesanti, meno critici, possono rallentare).
- Checkout/ACP (super-critico, da proteggere al massimo).
- Analytics/log (importante, ma può tollerare un po’ di ritardo).
In un’architettura più avanzata li deployi anche come cluster diversi con risorse dedicate, ma in questa lezione ci interessa l’idea: non permettere alle feature secondarie di “consumare tutto l’ossigeno”.
Questi tre pattern — timeout, circuit breaker e bulkheads — riguardano il modo in cui vai verso l’esterno, alle tue dipendenze. Ma c’è un’altra classe di minacce alla resilienza: i flussi in ingresso di eventi, che possono travolgerti anche con chiamate in uscita perfettamente configurate. L’esempio più tipico — le tempeste di webhook.
5. Tempeste di webhook: quando il mondo ti invia eventi più spesso di quanto tu sia pronto
Come si comportano i webhook nella realtà
La quarta fonte di problemi per la resilienza sono gli eventi in ingresso: webhook da ACP, Stripe e altri sistemi. Proprio loro possono scatenare una vera “tempesta”, anche se hai già configurato timeout, circuit breaker e bulkhead.
I webhook non sono una richiesta HTTP “on demand”, ma eventi “push” da sistemi esterni (Stripe, ACP, negozi esterni ecc.). Hanno alcune caratteristiche spiacevoli:
- Consegna almeno una volta (at-least-once) — quindi i duplicati sono inevitabili.
- L’ordine di consegna non è garantito.
- In caso di errori amano fare retry: prima dopo un secondo, poi dopo 10, poi dopo un minuto… finché non rispondi con 2xx.
- Nei picchi (per esempio, in periodo di saldi) arrivano a ondate, creando una “tempesta”.
Se il tuo handler non è idempotente e impiega troppo tempo, diventa il collo di bottiglia, tutta la coda si riempie e i retry rafforzano solo la tempesta. Di conseguenza puoi mettere in crisi il database, la coda, i pool di worker — e a catena il resto del sistema.
Principi base di protezione dalle tempeste
Ci sono alcune idee che aumentano molto le possibilità di sopravvivere a una tempesta:
Primo, queue-first, process-later. Idealmente un webhook in ingresso non dovrebbe eseguire lavoro pesante in modo sincrono. Invece, dovrebbe convalidare il prima possibile firma/formato, mettere il task in coda e rispondere 200 OK. L’elaborazione avviene asincronamente in un worker. Se ti serve una “conferma rapida” per ChatGPT, puoi mantenere un circuito di notifiche separato.
Secondo, idempotenza dell’handler. Un webhook ripetuto per la stessa operazione non deve “creare l’ordine un’altra volta” o “addebitare due volte”. Di solito si risolve memorizzando un idempotency key o eventId e verificando se abbiamo già elaborato quell’evento.
Terzo, rate limiting e circuit breaker al ricevitore. Anche se il mittente tempesta, tu puoi:
- limitare RPS per IP/sottoscrizione/endpoint,
- restituire temporaneamente 429 o 503 per rallentare i retry,
- usare un breaker per non riversare il flusso in un downstream rotto (per esempio, il DB degli ordini).
Esempio di handler di webhook Next.js in GiftGenius
Immaginiamo che abbiamo un ACP/sistema di pagamento che invia un webhook sullo stato dell’ordine a POST /api/commerce/webhook. Vogliamo:
- ricevere velocemente l’evento e metterlo in coda,
- non elaborarlo in modo sincrono,
- non rompersi con i duplicati.
Esempio semplificato (senza verifica della firma e senza una coda reale — questo sarà nei moduli su sicurezza e code):
// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
// Qui potremmo avere Redis/una coda; per ora simuliamo con un array
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // idempotenza (per demo)
export async function POST(req: NextRequest) {
const event = await req.json();
const eventId = event.id as string;
if (processedEvents.has(eventId)) {
return NextResponse.json({ ok: true, duplicate: true });
}
// Nella realtà qui ci sarebbe la verifica della firma e dello schema
inMemoryQueue.push(event); // mettiamo in coda per l'elaborazione in background
// Un worker in background elaborerà più tardi e segnerà l'ID come elaborato
return NextResponse.json({ ok: true });
}
Per ora è una pseudo-implementazione, ma contano due punti:
- La parte sincrona è il più leggera possibile.
- Mettiamo l’idempotenza attorno a event.id.
Nella vita reale farai:
- uso di una coda esterna (SQS, RabbitMQ, Kafka),
- memorizzazione degli eventi elaborati in un DB,
- verifica della firma del webhook e della versione del payload,
- eventualmente applicazione di un Bulkhead/Breaker attorno all’handler.
Come appare nel contesto di GiftGenius
Per GiftGenius, integrato con ACP/Stripe tramite webhook, la protezione dalle tempeste è particolarmente importante nelle stagioni di picco (Capodanno, Black Friday). Ci sono molti eventi:
- creazione di intent,
- conferma dei pagamenti,
- annullamenti,
- rimborsi.
Se il tuo handler inizia ad “allungarsi” (per esempio, a causa di chiamate a un’API esterna), rischi che:
- ACP inizi a fare retry,
- gli eventi arrivino a ondate,
- il DB degli ordini e il pool di worker si saturino.
Il pattern «queue first» + idempotenza + rate limiting in ingresso serve proprio da polizza contro questi scenari.
6. Come questi pattern lavorano insieme
Ora mettiamo insieme tutti questi pattern in uno scenario e vediamo come funzionano nel flusso reale “Suggerisci un regalo e finalizza subito l’ordine”.
Consideriamo la catena “ChatGPT → Gateway → Gift Service → Commerce → webhook” su un esempio:
L’utente in chat dice: «Suggerisci un regalo e finalizza subito l’ordine».
- Il modello decide di chiamare il tuo tool suggest_and_checkout.
- Il Gateway chiama il servizio gift tramite fetchWithTimeout e il bulkhead del servizio gift.
- Se il servizio gift si blocca — scatta il timeout; il breaker attorno ad esso, dopo un certo numero di errori, passa a open e le richieste successive ricevono subito l’errore MCP “gift_service_unavailable”.
- Se il servizio gift risponde, il Gateway chiama il servizio commerce (di nuovo con timeout e bulkhead separato).
- Qualsiasi problema con commerce attiva un circuit breaker separato, configurato più severamente rispetto a gift (perché il checkout è critico).
- Un ordine riuscito porta a un webhook da ACP sul tuo /api/commerce/webhook, che mette l’evento in coda e risponde rapidamente; i worker in background elaborano il pagamento, e i webhook ripetuti con lo stesso eventId vengono ignorati come duplicati.
Alla fine:
- Un servizio di selezione in stallo non abbatte il checkout.
- Un commerce in stallo non trasforma tutti i tool-calls in uno spinner di un minuto — ChatGPT riceve rapidamente un errore significativo.
- Le tempeste di webhook non rompono il tuo circuito HTTP principale.
- Controlli i punti di degradazione: meglio disattivare temporaneamente le raccomandazioni personalizzate che far saltare i pagamenti.
7. Piccola checklist pratica per la tua App (in forma narrativa)
In sintesi, in una tipica ChatGPT App con MCP/Gateway conviene passare in rassegna, nell’ordine, i seguenti punti.
Per prima cosa verifichi se hai timeout su tutte le chiamate esterne. Tutto il codice fetch, le richieste al DB e a LLM devono usare un wrapper come fetchWithTimeout con valori adeguati. È importante che non ci siano punti in cui la richiesta possa restare appesa all’infinito.
Poi identifichi le dipendenze più fragili. In genere sono sistemi di pagamento, ACP, grandi API esterne e talvolta il tuo DB degli ordini. Attorno a loro conviene aggiungere un circuit breaker, per proteggerti da valanghe di ripetizioni verso un servizio palesemente morto. Allo stesso tempo decidi subito come si comporterà ChatGPT quando il breaker è in stato open.
Dopo di ciò osservi le risorse come “compartimenti”. Tutto passa attraverso un unico connection pool e un unico worker pool, oppure le operazioni critiche (login, checkout) hanno propri limiti di parallelismo, indipendenti dal servizio di raccomandazione e dall’analytics? Se no — aggiungi la più semplice implementazione di bulkhead, almeno come limite grezzo di attività parallele.
Infine, conduci un audit di tutti i webhook in ingresso. Controlli se contengono un idempotency key o eventId, se non stai tentando di fare lavoro pesante in modo sincrono nell’handler HTTP e se sai sopravvivere a un’ondata di retry qualora il tuo downstream cadesse temporaneamente. Se no — sposti la logica in una coda e nei worker in background.
Questa sequenza di passi dà un notevole aumento della resilienza anche senza infrastrutture super complesse.
8. Errori tipici con timeouts, circuit breakers, bulkheads e tempeste di webhook
Errore n. 1: mancanza di timeout “da qualche parte in basso”.
Spesso gli sviluppatori impostano un timeout solo sul Gateway o solo sul frontend, dimenticando che dentro il backend ci sono ancora DB, API esterne e LLM. Di conseguenza la richiesta esterna sembra avere un timeout di 5 secondi, ma all’interno una chiamata al DB o al sistema di pagamento può restare appesa per minuti, bloccando il pool di connessioni e provocando guasti a cascata.
Errore n. 2: timeout giganteschi “per sicurezza”.
A volte si imposta un timeout di 60–120 secondi: “che almeno finisca”. Nel contesto ChatGPT è quasi sempre negativo. L’utente se ne va, il modello inizia ad allucinare e le tue risorse restano bloccate per tutto il tempo. Molto meglio un rifiuto onesto dopo 5–10 secondi con una descrizione comprensibile.
Errore n. 3: circuit breaker senza un UX ben pensato.
A volte si aggiunge il breaker “per forma”, ma quando scatta l’utente o il modello ricevono un incomprensibile 500, “ECONNREFUSED” o “axios error”. Il risultato è che GPT non riesce a spiegare adeguatamente cosa sta succedendo e inizia a inventare. Conviene pensare in anticipo a formulazioni di errore comprensibili sia alle persone sia al modello.
Errore n. 4: mescolare le risorse senza approccio bulkhead.
Scenario classico: un servizio di raccomandazioni (o di analytics) inizia a rallentare, consuma tutto il pool di connessioni al DB o il thread-pool e di conseguenza muoiono checkout e login. Tutto perché le risorse non sono separate. L’assenza di un approccio bulkhead fa sì che una feature secondaria possa abbattere l’intero prod.
Errore n. 5: trattare i webhook come richieste normali.
I principianti spesso scrivono un handler di webhook come un normale controller: lunga logica di business, chiamate a API esterne, assenza di idempotenza. Con retry e duplicati questo porta a doppia elaborazione degli eventi, stati strani degli ordini e cadute per carico durante una tempesta.
Errore n. 6: ignorare l’idempotenza negli scenari di commerce.
È particolarmente pericoloso quando il webhook di pagamento può creare di nuovo un ordine o modificarne lo stato due volte. Senza controllare l’idempotency key e senza memorizzare lo stato di elaborazione dell’evento, prima o poi otterrai addebiti doppi o strani duplicati degli ordini.
Errore n. 7: cercare di aggiustare tutto con setTimeout e “ritardi magici”.
A volte si tenta di aggirare race condition e problemi di tempesta con “aspettiamo 100 ms e andrà bene”. In pratica questo rende il comportamento ancora più instabile e non protegge affatto dai guasti reali. La strada giusta — timeout espliciti, circuit breaker, code e idempotenza, non magia con i ritardi.
Errore n. 8: assenza di prioritizzazione dei percorsi critici.
Quando checkout e login vivono negli stessi limiti di analytics o della logica di raccomandazione, qualsiasi sovraccarico può buttare giù allo stesso modo parti critiche e secondarie. In un design resiliente, checkout e auth sono “sacre”: per loro risorse separate, limiti separati, alert separati e SLO.
GO TO FULL VERSION