AIアプリケーションの開発において、ユーザー体験を左右する最大の要因の一つがレスポンス速度です。従来のサーバーレスアーキテクチャでは、リクエストが特定のリージョンに集中し、地理的に離れたユーザーほど遅延が大きくなるという課題がありました。
Cloudflare Workersは世界300以上のエッジロケーションでコードを実行でき、ユーザーに最も近いノードで処理を行います。これにClaude APIの高度な推論能力を組み合わせることで、低レイテンシかつスケーラブルなAIマイクロサービスを実現できます。
以下では Claude API と Cloudflare Workers で本番グレードの AI マイクロサービスを組む際の設計パターンと実装を、動作するコード例とともに追っていきます。
この記事で学べること:
- Cloudflare Workers上でClaude APIを呼び出す基本アーキテクチャ
- ストリーミングレスポンスによるリアルタイムUI連携
- KV・D1・R2を活用したキャッシュ戦略とコスト最適化
- レート制限・エラーハンドリング・リトライの実装
- 複数のAIエンドポイントを束ねるAPIゲートウェイパターン
対象読者: Claude APIの基本的な使い方を理解しており、Cloudflare Workersの経験がある中〜上級エンジニア。
基本アーキテクチャ — Workers から Claude API を呼び出す
まず、最もシンプルな構成として、Cloudflare Workerから直接Claude APIを呼び出すパターンを見ていきます。
プロジェクトのセットアップ
# Wrangler CLIでプロジェクトを作成
npm create cloudflare@latest claude-edge-service -- --type worker-typescript
cd claude-edge-service
# Anthropic SDKをインストール
npm install @anthropic-ai/sdk
# APIキーをシークレットとして登録
npx wrangler secret put ANTHROPIC_API_KEY
基本的なWorkerの実装
// src/index.ts
import Anthropic from "@anthropic-ai/sdk";
interface Env {
ANTHROPIC_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// CORSプリフライト対応
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
try {
const { message, model } = await request.json<{
message: string;
model?: string;
}>();
const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
// Claude APIを呼び出し
const response = await client.messages.create({
model: model || "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: message }],
});
const text =
response.content[0].type === "text" ? response.content[0].text : "";
return new Response(JSON.stringify({ response: text }), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("API Error:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
},
};
この基本パターンだけでも、世界中のエッジロケーションからClaude APIにアクセスできるエンドポイントが完成します。しかし、本番環境ではこれだけでは不十分です。次のセクション以降で、プロダクション品質に必要な要素を一つずつ追加していきます。
ストリーミングレスポンスの実装
Claude APIのストリーミング機能を活用すると、トークンが生成されるたびにクライアントにリアルタイムで送信できます。ユーザーは回答の完了を待たずに、テキストが流れるように表示される体験を得られます。
// src/streaming.ts — ストリーミング対応エンドポイント
import Anthropic from "@anthropic-ai/sdk";
interface Env {
ANTHROPIC_API_KEY: string;
}
async function handleStreamingRequest(
request: Request,
env: Env
): Promise<Response> {
const { message, systemPrompt } = await request.json<{
message: string;
systemPrompt?: string;
}>();
const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
// ReadableStreamを使ったSSE(Server-Sent Events)の実装
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
try {
const response = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 2048,
system: systemPrompt || "You are a helpful assistant.",
messages: [{ role: "user", content: message }],
});
// ストリーミングイベントを処理
response.on("text", (text) => {
const data = JSON.stringify({ type: "text_delta", text });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
});
// ストリーム完了時の処理
const finalMessage = await response.finalMessage();
const usage = {
input_tokens: finalMessage.usage.input_tokens,
output_tokens: finalMessage.usage.output_tokens,
};
const doneData = JSON.stringify({ type: "done", usage });
controller.enqueue(encoder.encode(`data: ${doneData}\n\n`));
controller.close();
} catch (error) {
const errData = JSON.stringify({
type: "error",
message: error instanceof Error ? error.message : "Unknown error",
});
controller.enqueue(encoder.encode(`data: ${errData}\n\n`));
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
},
});
}
export { handleStreamingRequest };
クライアント側の受信コード
// フロントエンド側(Next.js / React)
async function streamChat(message: string) {
const response = await fetch("https://your-worker.workers.dev/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n").filter((line) => line.startsWith("data: "));
for (const line of lines) {
const data = JSON.parse(line.slice(6));
if (data.type === "text_delta") {
// UIにテキストを追加表示
appendToChat(data.text);
} else if (data.type === "done") {
// トークン使用量をログに記録
console.log("Usage:", data.usage);
}
}
}
}
KV・D1・R2 を活用したキャッシュ戦略
Claude APIの呼び出しにはコストが発生するため、同じ入力に対するレスポンスをキャッシュすることで、コストを大幅に削減しつつレスポンス速度も向上させることができます。
Cloudflare KV によるレスポンスキャッシュ
// src/cache.ts — KVベースのキャッシュレイヤー
interface Env {
ANTHROPIC_API_KEY: string;
AI_CACHE: KVNamespace; // wrangler.tomlでバインド
}
// キャッシュキーの生成(入力内容のハッシュ化)
async function generateCacheKey(
model: string,
messages: Array<{ role: string; content: string }>,
systemPrompt?: string
): Promise<string> {
const payload = JSON.stringify({ model, messages, systemPrompt });
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(payload)
);
const hashArray = Array.from(new Uint8Array(hash));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
async function cachedCompletion(
env: Env,
model: string,
messages: Array<{ role: string; content: string }>,
systemPrompt?: string
): Promise<{ text: string; cached: boolean }> {
const cacheKey = await generateCacheKey(model, messages, systemPrompt);
// KVからキャッシュを検索
const cached = await env.AI_CACHE.get(cacheKey);
if (cached) {
return { text: cached, cached: true };
}
// キャッシュミス → Claude APIを呼び出し
const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
const response = await client.messages.create({
model,
max_tokens: 2048,
system: systemPrompt,
messages,
});
const text =
response.content[0].type === "text" ? response.content[0].text : "";
// レスポンスをKVにキャッシュ(TTL: 1時間)
await env.AI_CACHE.put(cacheKey, text, { expirationTtl: 3600 });
return { text, cached: false };
}
export { cachedCompletion, generateCacheKey };
wrangler.toml の設定
# wrangler.toml
name = "claude-edge-service"
main = "src/index.ts"
compatibility_date = "2026-03-01"
# KVバインディング(キャッシュ用)
[[kv_namespaces]]
binding = "AI_CACHE"
id = "your-kv-namespace-id"
# D1バインディング(使用量ログ用)
[[d1_databases]]
binding = "USAGE_DB"
database_name = "ai-usage-logs"
database_id = "your-d1-database-id"
D1 による使用量トラッキング
// src/usage-tracker.ts — D1でAPIコスト追跡
interface UsageRecord {
model: string;
input_tokens: number;
output_tokens: number;
cached: boolean;
endpoint: string;
timestamp: string;
}
async function trackUsage(db: D1Database, record: UsageRecord): Promise<void> {
await db
.prepare(
`INSERT INTO usage_logs (model, input_tokens, output_tokens, cached, endpoint, timestamp)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(
record.model,
record.input_tokens,
record.output_tokens,
record.cached ? 1 : 0,
record.endpoint,
record.timestamp
)
.run();
}
// 日次コストレポートの取得
async function getDailyCostReport(db: D1Database): Promise<Response> {
const result = await db
.prepare(
`SELECT
DATE(timestamp) as date,
model,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(CASE WHEN cached = 1 THEN 1 ELSE 0 END) as cache_hits,
COUNT(*) as total_requests
FROM usage_logs
WHERE timestamp >= datetime('now', '-7 days')
GROUP BY DATE(timestamp), model
ORDER BY date DESC`
)
.all();
return new Response(JSON.stringify(result.results), {
headers: { "Content-Type": "application/json" },
});
}
export { trackUsage, getDailyCostReport };
レート制限とエラーハンドリング
本番環境では、不正なリクエストやAPIの一時的な障害に対して堅牢に対応する必要があります。
スライディングウィンドウ型レート制限
// src/rate-limiter.ts — KVベースのレート制限
interface RateLimitConfig {
maxRequests: number; // ウィンドウ内の最大リクエスト数
windowSeconds: number; // ウィンドウの長さ(秒)
}
async function checkRateLimit(
kv: KVNamespace,
clientId: string,
config: RateLimitConfig
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const key = `ratelimit:${clientId}`;
const now = Math.floor(Date.now() / 1000);
// 現在のカウンターを取得
const data = await kv.get<{
count: number;
windowStart: number;
}>(key, "json");
if (!data || now - data.windowStart >= config.windowSeconds) {
// 新しいウィンドウを開始
await kv.put(
key,
JSON.stringify({ count: 1, windowStart: now }),
{ expirationTtl: config.windowSeconds }
);
return {
allowed: true,
remaining: config.maxRequests - 1,
resetAt: now + config.windowSeconds,
};
}
if (data.count >= config.maxRequests) {
return {
allowed: false,
remaining: 0,
resetAt: data.windowStart + config.windowSeconds,
};
}
// カウンターをインクリメント
await kv.put(
key,
JSON.stringify({ count: data.count + 1, windowStart: data.windowStart }),
{ expirationTtl: config.windowSeconds }
);
return {
allowed: true,
remaining: config.maxRequests - data.count - 1,
resetAt: data.windowStart + config.windowSeconds,
};
}
export { checkRateLimit, RateLimitConfig };
エクスポネンシャルバックオフ付きリトライ
// src/retry.ts — 指数バックオフリトライ
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// リトライ不可能なエラーは即座にスロー
if (isNonRetryableError(lastError)) {
throw lastError;
}
if (attempt < maxRetries) {
// 指数バックオフ + ジッター
const delay =
baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
function isNonRetryableError(error: Error): boolean {
const message = error.message.toLowerCase();
// 認証エラーやバリデーションエラーはリトライしない
return (
message.includes("authentication") ||
message.includes("invalid_api_key") ||
message.includes("invalid_request")
);
}
export { retryWithBackoff };
APIゲートウェイパターン — 複数エンドポイントの統合
実際のプロダクションでは、用途ごとに異なるプロンプトやモデルを使い分けるケースが一般的です。Cloudflare Workersをルーターとして使い、複数のAIエンドポイントを1つのサービスに統合するパターンを紹介します。
// src/router.ts — AIマイクロサービスのルーティング
import { cachedCompletion } from "./cache";
import { handleStreamingRequest } from "./streaming";
import { checkRateLimit } from "./rate-limiter";
import { trackUsage } from "./usage-tracker";
import { retryWithBackoff } from "./retry";
interface Env {
ANTHROPIC_API_KEY: string;
AI_CACHE: KVNamespace;
RATE_LIMIT: KVNamespace;
USAGE_DB: D1Database;
}
// エンドポイント定義
const ENDPOINTS: Record<
string,
{
model: string;
systemPrompt: string;
maxTokens: number;
}
> = {
"/api/chat": {
model: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
maxTokens: 2048,
},
"/api/summarize": {
model: "claude-haiku-4-5",
systemPrompt:
"Summarize the following text concisely. Output only the summary.",
maxTokens: 512,
},
"/api/code-review": {
model: "claude-sonnet-4-6",
systemPrompt:
"You are a senior code reviewer. Analyze the code and provide actionable feedback on bugs, performance, and best practices.",
maxTokens: 4096,
},
"/api/translate": {
model: "claude-haiku-4-5",
systemPrompt:
"Translate the following text to the target language. Output only the translation.",
maxTokens: 2048,
},
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// ヘルスチェック
if (path === "/health") {
return new Response(JSON.stringify({ status: "ok" }), {
headers: { "Content-Type": "application/json" },
});
}
// ストリーミングエンドポイント
if (path === "/api/stream") {
return handleStreamingRequest(request, env);
}
// 通常のAIエンドポイント
const endpointConfig = ENDPOINTS[path];
if (!endpointConfig) {
return new Response("Not found", { status: 404 });
}
// レート制限チェック
const clientIp = request.headers.get("CF-Connecting-IP") || "unknown";
const rateCheck = await checkRateLimit(env.RATE_LIMIT, clientIp, {
maxRequests: 60,
windowSeconds: 60,
});
if (!rateCheck.allowed) {
return new Response(
JSON.stringify({ error: "Rate limit exceeded" }),
{
status: 429,
headers: {
"Content-Type": "application/json",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": rateCheck.resetAt.toString(),
"Retry-After": Math.ceil(
rateCheck.resetAt - Date.now() / 1000
).toString(),
},
}
);
}
try {
const body = await request.json<{ message: string }>();
// キャッシュ付きClaude API呼び出し(リトライ付き)
const result = await retryWithBackoff(() =>
cachedCompletion(
env,
endpointConfig.model,
[{ role: "user", content: body.message }],
endpointConfig.systemPrompt
)
);
// 使用量を非同期で記録(レスポンスをブロックしない)
// 期待する出力: D1のusage_logsテーブルに1行追加される
void trackUsage(env.USAGE_DB, {
model: endpointConfig.model,
input_tokens: 0, // キャッシュヒット時は0
output_tokens: 0,
cached: result.cached,
endpoint: path,
timestamp: new Date().toISOString(),
});
return new Response(
JSON.stringify({
response: result.text,
cached: result.cached,
}),
{
headers: {
"Content-Type": "application/json",
"X-RateLimit-Remaining": rateCheck.remaining.toString(),
"Access-Control-Allow-Origin": "*",
},
}
);
} catch (error) {
return new Response(
JSON.stringify({
error: "Service temporarily unavailable",
}),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
},
};
デプロイとモニタリング
wrangler.toml の本番設定
# wrangler.toml — 本番環境設定
name = "claude-edge-service"
main = "src/router.ts"
compatibility_date = "2026-03-01"
# 本番環境設定
[env.production]
name = "claude-edge-service-prod"
routes = [
{ pattern = "api.example.com/*", zone_name = "example.com" }
]
# KV(キャッシュ + レート制限)
[[kv_namespaces]]
binding = "AI_CACHE"
id = "cache-namespace-id"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "ratelimit-namespace-id"
# D1(使用量ログ)
[[d1_databases]]
binding = "USAGE_DB"
database_name = "ai-usage-logs"
database_id = "database-id"
デプロイコマンド
# 開発環境でのテスト
npx wrangler dev
# 本番環境へデプロイ
npx wrangler deploy --env production
# デプロイ後の動作確認
# 期待する出力: {"response": "Hello! ...", "cached": false}
curl -X POST https://api.example.com/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, Claude!"}'
まとめ — エッジAIの可能性を広げよう
Claude APIとCloudflare Workersの組み合わせは、AIアプリケーションのデプロイと運用における新しい選択肢を提供します。この記事で紹介したパターンをまとめると:
- 基本呼び出し: WorkerからClaude APIへの直接リクエストで、グローバルなエンドポイントを数分で構築
- ストリーミング: SSEを活用したリアルタイムレスポンスでUXを向上
- キャッシュ戦略: KVによるレスポンスキャッシュで、コスト削減とレイテンシ短縮を同時に実現
- 堅牢性: レート制限・リトライ・エラーハンドリングで本番環境の安定性を確保
- APIゲートウェイ: 複数のAIエンドポイントを統合し、マイクロサービスとして運用
実際にプロジェクトで試す際は、まず1つのエンドポイント(例: /api/summarize)から始めて、段階的に拡張していくアプローチがお勧めです。Cloudflare Workersの無料枠(1日10万リクエスト)を活用すれば、低コストで検証を進められます。
エッジコンピューティングとAIの融合は、今後さらに加速していく分野です。ぜひ今回のコードをベースに、独自のAIマイクロサービスを構築してみてください。