Ridurre i Costi delle API LLM: 7 Strategie Pratiche
Perché Ottimizzare i Costi delle API LLM nel 2026
Nel 2026, integrare modelli linguistici nei propri prodotti non è più un’opzione: è un requisito competitivo. Eppure, la prima fattura mensile di API usage può essere uno shock. Un’applicazione che gestisce migliaia di richieste al giorno su Claude Opus o GPT-4 può tranquillamente costare centinaia di dollari al mese, con picchi difficili da prevedere.
Il problema non è il costo in sé: è l’assenza di strategia. La maggior parte dei developer integra il modello più potente disponibile, lo chiama ogni volta che serve, passa prompt enormi con tutto il contesto possibile, e si chiede perché il conto lievita. La buona notizia è che esistono sette strategie concrete che, applicate insieme, possono ridurre i costi delle API LLM fino all’80% senza degradare la qualità dell’output percepita dall’utente finale.
In questa guida vedremo ogni strategia con esempi di codice reali, numeri, e le scelte architetturali che fanno davvero la differenza. Se stai costruendo un Micro SaaS o integrando AI in un prodotto esistente, questo articolo ti eviterà errori costosi.
Strategia 1: Prompt Caching — Il Risparmio Più Immediato
Il prompt caching è la tecnica più impattante disponibile oggi su Claude API. Consente di memorizzare porzioni del contesto (system prompt, documenti, esempi few-shot) e riutilizzarle nelle chiamate successive pagando solo i cache read tokens, che costano circa il 10% dei token standard. Come abbiamo approfondito nell’articolo dedicato al Prompt Caching in Claude API, questa tecnica può ridurre i costi fino al 90% nei pattern ad alto riutilizzo del contesto.
Il pattern tipico è un system prompt lungo con istruzioni, regole di business, e documentazione interna. Senza caching, ogni messaggio dell’utente riprocessa tutto quel contesto. Con caching, lo processi una volta e poi riutilizzi:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// System prompt lungo con documentazione, regole, esempi
const SYSTEM_PROMPT = `
Sei un assistente specializzato per l'e-commerce XYZ.
[... 2000 token di contesto aziendale, catalogo, policy ...]
`;
async function chatWithCaching(userMessage: string) {
const response = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 1024,
system: [
{
type: "text",
text: SYSTEM_PROMPT,
// Marca questo blocco per il caching
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userMessage }],
});
// Controlla l'utilizzo del cache
const usage = response.usage;
console.log("Cache read tokens:", usage.cache_read_input_tokens);
console.log("Cache write tokens:", usage.cache_creation_input_tokens);
console.log("Input tokens normali:", usage.input_tokens);
return response.content[0].type === "text" ? response.content[0].text : "";
}
Con un system prompt da 2.000 token e 1.000 messaggi al giorno, il risparmio mensile è significativo: invece di pagare 2.000 token x 1.000 x 30 giorni, paghi 2.000 token una volta (cache write) e poi solo lettura cache a costo ridotto.
Strategia 2: Model Routing — Usa il Modello Giusto per il Task Giusto
Uno degli errori più costosi è usare sempre il modello più capace. Claude Opus costa 10-15x rispetto a Claude Haiku per task identici. Il model routing consiste nel classificare le richieste in ingresso e scegliere dinamicamente il modello più adeguato.
Come abbiamo visto nel Confronto LLM 2026, ogni modello ha i suoi punti di forza: Haiku eccelle nei task di estrazione e classificazione semplice, mentre Opus è necessario solo per ragionamento complesso e codice avanzato.
type TaskComplexity = "simple" | "medium" | "complex";
interface ModelConfig {
model: string;
costPer1MTokens: number;
}
const MODEL_TIERS: Record<TaskComplexity, ModelConfig> = {
simple: {
model: "claude-haiku-4-5",
costPer1MTokens: 0.25,
},
medium: {
model: "claude-sonnet-4-5",
costPer1MTokens: 3.0,
},
complex: {
model: "claude-opus-4-5",
costPer1MTokens: 15.0,
},
};
function classifyTask(prompt: string): TaskComplexity {
const complexKeywords = [
"analizza",
"ragiona",
"scrivi codice",
"architettura",
"confronta",
"valuta criticamente",
];
const mediumKeywords = [
"riassumi",
"spiega",
"traduci",
"parafrasa",
"riscrivi",
];
const lowerPrompt = prompt.toLowerCase();
if (complexKeywords.some((kw) => lowerPrompt.includes(kw))) {
return "complex";
}
if (mediumKeywords.some((kw) => lowerPrompt.includes(kw))) {
return "medium";
}
return "simple";
}
async function smartChat(client: Anthropic, userMessage: string) {
const complexity = classifyTask(userMessage);
const { model } = MODEL_TIERS[complexity];
console.log(`Task classificato come "${complexity}" -> usando ${model}`);
const response = await client.messages.create({
model,
max_tokens: 1024,
messages: [{ role: "user", content: userMessage }],
});
return response;
}
Con un routing ben calibrato su un’applicazione reale, il 70-80% delle richieste può essere gestito dal tier simple, riducendo drasticamente il costo medio per richiesta. Il costo di un classificatore Haiku (usato per il routing) è trascurabile rispetto al risparmio ottenuto.
Strategia 3: Batching e Request Aggregation
Molte applicazioni fanno chiamate API per ogni micro-azione dell’utente: ogni keystroke, ogni hover, ogni piccola interazione. Il batching consiste nell’aggregare più richieste simili in una sola chiamata, riducendo overhead e token di contesto duplicati.
Questo pattern è particolarmente utile nei sistemi di automazione — se stai costruendo workflow come quelli descritti in Claude API per automatizzare workflow, il batching può ridurre le chiamate API di un ordine di grandezza.
class BatchProcessor {
private queue: Array<{
id: string;
prompt: string;
resolve: (result: string) => void;
reject: (err: Error) => void;
}> = [];
private timer: ReturnType<typeof setTimeout> | null = null;
private readonly BATCH_SIZE = 10;
private readonly BATCH_DELAY_MS = 200;
constructor(private client: Anthropic) {}
async process(id: string, prompt: string): Promise<string> {
return new Promise((resolve, reject) => {
this.queue.push({ id, prompt, resolve, reject });
if (this.queue.length >= this.BATCH_SIZE) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.BATCH_DELAY_MS);
}
});
}
private async flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const batch = this.queue.splice(0, this.BATCH_SIZE);
if (batch.length === 0) return;
// Raggruppa tutti i prompt in un unico messaggio strutturato
const combinedPrompt = batch
.map(
(item, i) =>
`--- TASK ${i + 1} (id: ${item.id}) ---
${item.prompt}`
)
.join("
");
try {
const response = await this.client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 4096,
messages: [
{
role: "user",
content: `Elabora i seguenti ${batch.length} task indipendenti.
Per ogni task rispondi con: --- RISPOSTA TASK N ---
seguito dalla risposta.
${combinedPrompt}`,
},
],
});
const fullText =
response.content[0].type === "text" ? response.content[0].text : "";
// Parsa le risposte e risolvi le promise corrispondenti
batch.forEach((item, i) => {
const pattern = new RegExp(
`--- RISPOSTA TASK ${i + 1} ---([\s\S]*?)(?=--- RISPOSTA TASK ${i + 2} ---|$)`
);
const match = fullText.match(pattern);
if (match) {
item.resolve(match[1].trim());
} else {
item.reject(new Error(`Risposta task ${i + 1} non trovata`));
}
});
} catch (err) {
batch.forEach((item) =>
item.reject(err instanceof Error ? err : new Error(String(err)))
);
}
}
}
Strategia 4: Output Caching e Memoizzazione
Non tutte le richieste richiedono una risposta fresca. Se due utenti chiedono la stessa cosa — o se lo stesso utente fa query simili — non ha senso chiamare l’API ogni volta. L’output caching memorizza le risposte già generate e le restituisce istantaneamente quando la richiesta è identica o semanticamente equivalente.
Esistono due livelli di caching:
- Exact match caching: hash del prompt, lookup in Redis/memoria. Zero costo API per hit.
- Semantic caching: embedding della query, ricerca per similarità in un vector store. Richiede un sistema RAG ma può coprire richieste parafrasate.
import crypto from "crypto";
interface CacheEntry {
response: string;
createdAt: number;
ttl: number;
}
class LLMCache {
private store = new Map<string, CacheEntry>();
private readonly DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 ore
private hashPrompt(
prompt: string,
model: string,
systemPrompt?: string
): string {
const payload = JSON.stringify({ prompt, model, systemPrompt });
return crypto.createHash("sha256").update(payload).digest("hex");
}
get(prompt: string, model: string, systemPrompt?: string): string | null {
const key = this.hashPrompt(prompt, model, systemPrompt);
const entry = this.store.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.createdAt > entry.ttl) {
this.store.delete(key);
return null;
}
return entry.response;
}
set(
prompt: string,
model: string,
response: string,
ttlMs?: number,
systemPrompt?: string
): void {
const key = this.hashPrompt(prompt, model, systemPrompt);
this.store.set(key, {
response,
createdAt: Date.now(),
ttl: ttlMs ?? this.DEFAULT_TTL_MS,
});
}
}
const llmCache = new LLMCache();
async function cachedChat(
client: Anthropic,
model: string,
userMessage: string,
systemPrompt?: string
): Promise<string> {
// Controlla cache prima di chiamare l'API
const cached = llmCache.get(userMessage, model, systemPrompt);
if (cached) {
console.log("Cache HIT - risposta gratuita!");
return cached;
}
// Cache MISS: chiama l'API
const response = await client.messages.create({
model,
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
});
const text =
response.content[0].type === "text" ? response.content[0].text : "";
// Salva in cache
llmCache.set(userMessage, model, text, undefined, systemPrompt);
return text;
}
Nei sistemi di FAQ automation o chatbot aziendali, dove molte domande si ripetono, il cache hit rate può raggiungere il 60-70%. Ogni hit è un risparmio del 100% per quella richiesta.
Strategia 5: Ottimizzazione dei Token in Input e Output
La qualità del prompt determina non solo la qualità dell’output, ma anche il numero di token consumati. Prompt verbosi, ripetitivi o mal strutturati sprecano token senza migliorare i risultati. La compressione del contesto è un’arte che vale la pena imparare.
Alcune regole pratiche:
- Tronca la conversation history: mantieni solo gli ultimi N turni rilevanti, non tutta la storia.
- Usa max_tokens in modo intelligente: se ti aspetti una risposta breve (sì/no, classificazione), imposta
max_tokensbasso. Risparmi sui token di output. - Compatta i documenti: invece di passare un intero PDF, estrai solo le sezioni rilevanti con RAG. Come descritto in RAG con Claude API, recuperare solo i chunk pertinenti riduce drasticamente i token in input.
- Evita formati verbosi: se hai bisogno di JSON strutturato, specifica lo schema esatto invece di lasciare che il modello “inventi” un formato.
interface Message {
role: "user" | "assistant";
content: string;
}
// Mantieni solo gli ultimi N turni della conversazione
function trimHistory(
messages: Message[],
maxTurns: number = 6
): Message[] {
if (messages.length <= maxTurns * 2) return messages;
// Mantieni sempre il primo messaggio (contesto iniziale) e gli ultimi N turni
const firstMessage = messages[0];
const recentMessages = messages.slice(-(maxTurns * 2 - 1));
return [firstMessage, ...recentMessages];
}
// Stima rapida del costo prima di inviare
function estimateCost(
promptTokens: number,
expectedOutputTokens: number,
model: string
): number {
const pricing: Record<string, { input: number; output: number }> = {
"claude-haiku-4-5": { input: 0.25, output: 1.25 },
"claude-sonnet-4-5": { input: 3.0, output: 15.0 },
"claude-opus-4-5": { input: 15.0, output: 75.0 },
};
const p = pricing[model] ?? pricing["claude-sonnet-4-5"];
const inputCost = (promptTokens / 1_000_000) * p.input;
const outputCost = (expectedOutputTokens / 1_000_000) * p.output;
return inputCost + outputCost;
}
// Esempio: classificazione con max_tokens basso
async function classifySentiment(
client: Anthropic,
text: string
): Promise<"positive" | "negative" | "neutral"> {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 10, // Solo "positive", "negative", o "neutral"
messages: [
{
role: "user",
content: `Classifica il sentiment del seguente testo con UNA SOLA PAROLA (positive/negative/neutral):
"${text}"`,
},
],
});
const result =
response.content[0].type === "text"
? response.content[0].text.trim().toLowerCase()
: "neutral";
if (result.includes("positive")) return "positive";
if (result.includes("negative")) return "negative";
return "neutral";
}
Strategia 6: Streaming con Early Termination
Lo streaming non è solo un miglioramento UX: se implementato correttamente, può ridurre i costi. Con lo streaming puoi interrompere la risposta non appena hai ottenuto ciò che ti serve, senza pagare i token rimanenti. Come illustrato nell’articolo sullo Streaming Response con Claude API, l’early termination è particolarmente utile per estrazioni strutturate.
// Estrazione con early termination: fermati appena hai il JSON
async function extractWithEarlyStop(
client: Anthropic,
text: string
): Promise<{ name: string; email: string; phone: string }> {
let buffer = "";
let extracted = false;
const stream = await client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 200,
messages: [
{
role: "user",
content: `Estrai nome, email e telefono da questo testo e restituisci SOLO un JSON valido:
"${text}"
Formato atteso:
{"name": "...", "email": "...", "phone": "..."}`,
},
],
});
for await (const chunk of stream) {
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
buffer += chunk.delta.text;
// Controlla se abbiamo già un JSON completo
if (buffer.includes("}") && !extracted) {
const jsonMatch = buffer.match(/\{[^}]+\}/);
if (jsonMatch) {
try {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed.name || parsed.email || parsed.phone) {
extracted = true;
// In produzione qui potresti abortire lo stream
// per risparmiare i token rimanenti
stream.abort();
return parsed;
}
} catch {
// JSON non ancora completo, continua
}
}
}
}
}
// Fallback: parsa tutto il buffer
const jsonMatch = buffer.match(/\{[^}]+\}/);
if (jsonMatch) return JSON.parse(jsonMatch[0]);
throw new Error("Estrazione fallita");
}
Strategia 7: Monitoring e Cost Alerting in Produzione
Tutte le strategie precedenti perdono valore senza un sistema di monitoring dei costi in tempo reale. Sapere quanto stai spendendo, per quale feature, con quale modello è fondamentale per iterare e ottimizzare.
La struttura minima da implementare:
- Logga ogni chiamata API con: modello, token input/output, feature che l’ha generata, user ID (se applicabile), timestamp
- Aggrega per feature e calcola il costo medio per richiesta
- Imposta alert quando il costo giornaliero supera una soglia
- Usa i dati per decidere quali feature beneficiano di caching più aggressivo
interface UsageRecord {
timestamp: Date;
model: string;
feature: string;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
costUSD: number;
durationMs: number;
}
const PRICING: Record<string, { input: number; output: number; cacheRead: number; cacheWrite: number }> = {
"claude-haiku-4-5": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.30 },
"claude-sonnet-4-5": { input: 3.0, output: 15.0, cacheRead: 0.30, cacheWrite: 3.75 },
"claude-opus-4-5": { input: 15.0, output: 75.0, cacheRead: 1.50, cacheWrite: 18.75 },
};
function calculateCost(
model: string,
inputTokens: number,
outputTokens: number,
cacheReadTokens = 0,
cacheWriteTokens = 0
): number {
const p = PRICING[model] ?? PRICING["claude-sonnet-4-5"];
return (
(inputTokens / 1_000_000) * p.input +
(outputTokens / 1_000_000) * p.output +
(cacheReadTokens / 1_000_000) * p.cacheRead +
(cacheWriteTokens / 1_000_000) * p.cacheWrite
);
}
class LLMUsageTracker {
private records: UsageRecord[] = [];
private dailyBudgetUSD: number;
private dailyTotal = 0;
private lastReset = new Date().toDateString();
constructor(dailyBudgetUSD: number = 50) {
this.dailyBudgetUSD = dailyBudgetUSD;
}
track(record: Omit<UsageRecord, "timestamp">): void {
const today = new Date().toDateString();
if (today !== this.lastReset) {
this.dailyTotal = 0;
this.lastReset = today;
}
this.dailyTotal += record.costUSD;
this.records.push({ ...record, timestamp: new Date() });
if (this.dailyTotal > this.dailyBudgetUSD) {
console.error(
`ALERT: Budget giornaliero superato! Speso: $${this.dailyTotal.toFixed(4)}`
);
// In produzione: invia notifica Slack/email
}
}
getStatsByFeature(): Record<string, { calls: number; totalCost: number; avgCost: number }> {
const stats: Record<string, { calls: number; totalCost: number }> = {};
this.records.forEach((r) => {
if (!stats[r.feature]) stats[r.feature] = { calls: 0, totalCost: 0 };
stats[r.feature].calls++;
stats[r.feature].totalCost += r.costUSD;
});
return Object.fromEntries(
Object.entries(stats).map(([feature, data]) => [
feature,
{ ...data, avgCost: data.totalCost / data.calls },
])
);
}
}
Implementando un tracker di questo tipo puoi scoprire, per esempio, che una feature di autocomplete consuma il 40% del budget ma è usata solo dal 5% degli utenti: un candidato perfetto per caching aggressivo o downgrade del modello.
FAQ e Domande Frequenti
Qual è la strategia più efficace per ridurre i costi delle API LLM?
Il prompt caching è generalmente la strategia con il ROI più alto, soprattutto se la tua applicazione usa system prompt lunghi o documenti di contesto ripetuti. Combinato con il model routing — che assegna automaticamente il modello più economico al task più semplice — può ridurre i costi del 70-80% senza alcuna degradazione percepita dall’utente. Il punto di partenza ideale è misurare prima dove si concentra il tuo spend attuale, poi applicare le ottimizzazioni per ordine di impatto.
Il batching degrada la qualità delle risposte?
No, se implementato correttamente. Il batching aggrega prompt distinti in una singola chiamata API strutturata: il modello elabora ogni task in modo indipendente, e la qualità della singola risposta non è influenzata dalla presenza degli altri task. L’unico rischio è un’interfaccia di parsing mal progettata che confonda le risposte — ma con un formato chiaro e test adeguati è facilmente gestibile. Il vantaggio principale è la riduzione del numero di chiamate API e dell’overhead di rete.
Come scelgo la TTL giusta per l’output caching?
Dipende dalla natura del contenuto. Per contenuti statici (FAQ, documentazione di prodotto, descrizioni) una TTL di 24-72 ore è appropriata. Per contenuti semi-dinamici (riepiloghi personalizzati, analisi) 1-4 ore. Per contenuti in tempo reale (notizie, prezzi, disponibilità) il caching non è indicato. Una buona pratica è implementare una chiave di invalidazione per aggiornare il cache manualmente quando i dati sottostanti cambiano, indipendentemente dalla TTL.
Conviene usare modelli self-hosted per risparmiare sulle API?
Per volumi molto elevati (milioni di richieste al mese) o per use case con esigenze di privacy strette, i modelli self-hosted come Llama 3 su hardware proprio possono essere più economici a lungo termine. Tuttavia, il costo nascosto è l’infrastruttura GPU, il mantenimento, le ottimizzazioni e la gestione della latenza. Per la maggior parte dei developer indie e delle piccole aziende, le API gestite restano più convenienti anche considerando i costi variabili. La scelta si giustifica tipicamente oltre le 50-100 milioni di token mensili.
Conclusione
Ridurre i costi delle API LLM non significa sacrificare la qualità: significa usare i modelli in modo intelligente. Le sette strategie descritte in questa guida — prompt caching, model routing, batching, output caching, ottimizzazione dei token, streaming con early termination e monitoring — non sono soluzioni isolate. Funzionano meglio combinate e applicate in base alla specifica architettura del tuo prodotto.
Il punto di partenza è sempre il monitoring: senza dati reali su dove stai spendendo, qualsiasi ottimizzazione è cieca. Una volta che hai visibilità sullo spend per feature e per modello, le priorità emergono da sole. Inizia con il prompt caching se hai system prompt lunghi, con il model routing se hai task eterogenei, e con l’output caching se hai domande che si ripetono. Con un approccio iterativo, risparmiare il 70-80% del budget API è un obiettivo realistico già nelle prime settimane di ottimizzazione.
Suggerimenti e Risorse
🔧 Strumento: Usa l’endpoint
/v1/usagedi Anthropic (o il dashboard) per esportare i dati di utilizzo e costruire report personalizzati per feature. Combinalo con un Google Sheet condiviso per il team per avere visibilità condivisa sul budget.
💡 Pro tip: Prima di ottimizzare, misura. Implementa il tracking per almeno 7 giorni per avere un baseline affidabile. Spesso scoprirai che il 20% delle feature consuma l’80% del budget — concentra lì le ottimizzazioni per il massimo impatto con il minimo sforzo.
🎯 Prossimo step: Se stai costruendo agenti AI con tool use, il costo può esplodere rapidamente per il multi-step reasoning. Leggi la guida su Tool Use con Claude API per capire come strutturare gli agenti in modo cost-effective, limitando il numero di tool call per task.

