Production Deployment
Shipping a Claude-based system to production involves more considerations than a typical software deployment. This chapter covers the challenges unique to AI: non-deterministic responses, cost, API reliability, and latency.
Production Architectureโ
[Client]
โ
[API Gateway] โ auth, rate limiting, logging
โ
[AI Middleware Layer]
โโโ Prompt template management
โโโ Cache layer (Redis)
โโโ Queue (Bull/BullMQ)
โโโ Cost tracking
โ
[Anthropic API] โ Claude models
โ
[Response post-processing]
โโโ Output validation
โโโ Format conversion
โโโ Log storage
Environment Variable Managementโ
# .env.production
ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_MODEL=claude-sonnet-5
CLAUDE_MAX_TOKENS=4096
CLAUDE_TIMEOUT_MS=30000
CLAUDE_MAX_RETRIES=3
# Cost limits
DAILY_BUDGET_USD=100
MONTHLY_BUDGET_USD=2000
# Caching
REDIS_URL=redis://...
CACHE_TTL_SECONDS=3600
// config.ts
export const config = {
anthropic: {
apiKey: process.env.ANTHROPIC_API_KEY!,
model: process.env.CLAUDE_MODEL || "claude-sonnet-5",
maxTokens: parseInt(process.env.CLAUDE_MAX_TOKENS || "4096"),
timeoutMs: parseInt(process.env.CLAUDE_TIMEOUT_MS || "30000"),
maxRetries: parseInt(process.env.CLAUDE_MAX_RETRIES || "3")
},
budget: {
dailyUsd: parseFloat(process.env.DAILY_BUDGET_USD || "100"),
monthlyUsd: parseFloat(process.env.MONTHLY_BUDGET_USD || "2000")
}
};
Response Cachingโ
Avoid duplicate API calls for identical requests:
import { Redis } from "ioredis";
import Anthropic from "@anthropic-ai/sdk";
import crypto from "crypto";
const redis = new Redis(process.env.REDIS_URL);
const client = new Anthropic();
function getCacheKey(prompt: string, model: string): string {
return `claude:${crypto
.createHash("sha256")
.update(`${model}:${prompt}`)
.digest("hex")}`;
}
async function cachedQuery(
prompt: string,
ttlSeconds = 3600
): Promise<string> {
const cacheKey = getCacheKey(prompt, config.anthropic.model);
// Check the cache
const cached = await redis.get(cacheKey);
if (cached) {
console.log("โ
Cache hit");
return cached;
}
// Call the API
const response = await client.messages.create({
model: config.anthropic.model,
max_tokens: config.anthropic.maxTokens,
messages: [{ role: "user", content: prompt }]
});
const text = response.content[0].type === "text"
? response.content[0].text
: "";
// Store in the cache
await redis.setex(cacheKey, ttlSeconds, text);
return text;
}
Cost Monitoringโ
import Anthropic from "@anthropic-ai/sdk";
interface UsageRecord {
timestamp: Date;
model: string;
inputTokens: number;
outputTokens: number;
estimatedCostUsd: number;
requestId: string;
}
// Per-model token cost (USD per 1M tokens, as of 2026)
const TOKEN_COSTS: Record<string, { input: number; output: number }> = {
"claude-opus-4-8": { input: 5, output: 25 },
"claude-sonnet-5": { input: 3, output: 15 },
"claude-haiku-4-5": { input: 0.25, output: 1.25 }
};
function calculateCost(
model: string,
inputTokens: number,
outputTokens: number
): number {
const costs = TOKEN_COSTS[model] || TOKEN_COSTS["claude-sonnet-5"];
return (inputTokens * costs.input + outputTokens * costs.output) / 1_000_000;
}
class CostTracker {
private records: UsageRecord[] = [];
private dailyTotal = 0;
async track(
response: Anthropic.Message,
requestId: string
): Promise<void> {
const cost = calculateCost(
response.model,
response.usage.input_tokens,
response.usage.output_tokens
);
const record: UsageRecord = {
timestamp: new Date(),
model: response.model,
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
estimatedCostUsd: cost,
requestId
};
this.records.push(record);
this.dailyTotal += cost;
// Warn when the daily budget is exceeded
if (this.dailyTotal > config.budget.dailyUsd * 0.8) {
console.warn(`โ ๏ธ Over 80% of the daily budget: $${this.dailyTotal.toFixed(2)}`);
}
// In practice, persist to a database
await this.saveToDatabase(record);
}
getDailyTotal(): number {
return this.dailyTotal;
}
private async saveToDatabase(record: UsageRecord) {
// DB persistence logic
}
}
Queue-Based Processingโ
Handle high request volume reliably with a queue:
import Bull from "bull";
import Anthropic from "@anthropic-ai/sdk";
const aiQueue = new Bull("ai-tasks", {
redis: process.env.REDIS_URL,
defaultJobOptions: {
attempts: 3,
backoff: { type: "exponential", delay: 2000 },
removeOnComplete: true,
removeOnFail: false
}
});
// Worker definition
aiQueue.process(3, async (job) => { // Process up to 3 concurrently
const { prompt, userId, requestId } = job.data;
try {
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }]
});
const text = response.content[0].type === "text"
? response.content[0].text
: "";
// Store the result (e.g. notify the client over a websocket)
await notifyUser(userId, requestId, text);
return text;
} catch (error) {
console.error(`Request failed [${requestId}]:`, error);
throw error; // Bull will retry
}
});
// Event handlers
aiQueue.on("completed", (job) => {
console.log(`โ
Completed: ${job.id}`);
});
aiQueue.on("failed", (job, err) => {
console.error(`โ Failed: ${job.id} - ${err.message}`);
});
// Enqueue a task
async function submitAITask(prompt: string, userId: string) {
const requestId = crypto.randomUUID();
await aiQueue.add({ prompt, userId, requestId });
return requestId;
}
Observabilityโ
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("claude-service");
async function tracedClaudeCall(prompt: string): Promise<string> {
return tracer.startActiveSpan("claude.messages.create", async (span) => {
try {
span.setAttributes({
"claude.model": config.anthropic.model,
"claude.prompt_length": prompt.length,
"user.id": getCurrentUserId()
});
const client = new Anthropic();
const response = await client.messages.create({
model: config.anthropic.model,
max_tokens: config.anthropic.maxTokens,
messages: [{ role: "user", content: prompt }]
});
span.setAttributes({
"claude.input_tokens": response.usage.input_tokens,
"claude.output_tokens": response.usage.output_tokens,
"claude.stop_reason": response.stop_reason
});
span.setStatus({ code: SpanStatusCode.OK });
return response.content[0].type === "text"
? response.content[0].text
: "";
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message
});
throw error;
} finally {
span.end();
}
});
}
Health Checks and Alertsโ
import express from "express";
const app = express();
// Health check endpoint
app.get("/health", async (req, res) => {
const checks = {
api: false,
redis: false,
queue: false
};
// Check the Anthropic API connection
try {
const client = new Anthropic();
await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 10,
messages: [{ role: "user", content: "ping" }]
});
checks.api = true;
} catch {}
// Check the Redis connection
try {
await redis.ping();
checks.redis = true;
} catch {}
// Check the queue state
try {
const waiting = await aiQueue.getWaitingCount();
checks.queue = waiting < 1000; // Over 1000 is abnormal
} catch {}
const allHealthy = Object.values(checks).every(Boolean);
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? "healthy" : "degraded",
checks,
timestamp: new Date().toISOString()
});
});
Production Deployment Checklistโ
Before you deploy
Security
- API keys live only in environment variables, never in code
- Input validation to defend against prompt injection
- Output sanitization (XSS prevention)
- Per-request user authentication/authorization
Reliability
- Retry logic (exponential backoff)
- Timeouts configured
- Circuit breaker pattern applied
- Queue to absorb traffic spikes
Cost
- Daily/monthly budget alerts configured
- Token usage tracking
- Caching to prevent duplicate requests
- The right model chosen for each task
Observability
- Distributed tracing (OpenTelemetry)
- Metrics dashboard (Grafana/Datadog)
- Error alerting (PagerDuty/Slack)
- Response quality monitoring
Found an issue on this page? Report it โ