最初のトークンまでの時間(TTFT)の測定方法
この記事では、AIシステムにおける最初のトークンまでの時間(TTFT)の測定方法、従来のWeb APIパフォーマンス測定との根本的な違い、およびPython、Node.js、Apache JMeterを使用したLLMワークロードの計装方法について説明します。
AIシステムにおいて、最初のトークンまでの時間(TTFT)は、ユーザーが感じる応答性を測る重要な指標です。従来のHTTP応答時間とは異なり、TTFTはプロンプトリクエストを送信してから応答ストリームの最初のトークンを受信するまでの経過時間を測定します。ストリーミングLLM APIでは、ユーザーは応答の終了時ではなく開始時に応答性を知覚するため、この違いは重要です。
従来のWeb APIパフォーマンス指標(応答時間、スループット、エラー率)は、LLMに適用すると完全に機能しなくなります。OpenAI、Anthropic、またはローカルでホストされたOllamaエンドポイントを呼び出すと、他のREST APIと同様に見えますが、その応答時間は誤解を招きます。LLMは応答全体を一度に計算するのではなく、トークンを1つずつ生成してストリーミングします。従来のパフォーマンスツールが記録するのは、最初のトークンではなく、最後のトークンが到着した時刻です。ユーザーが4秒間空白の画面を見つめ、その後テキストの壁が到着する体験は、たとえ総経過時間が5秒でSLA内であっても、ひどいものです。
完全なLLMパフォーマンス測定戦略では、TTFT(知覚応答性)、TTLT(エンドツーエンドレイテンシ)、トークンスループット(生成速度)、トークン間レイテンシ(ストリーミングの滑らかさ)、グッドプット(実際のシステム容量)、ジッター(同時負荷下での一貫性)など、複数の指標を追跡する必要があります。TTFTは主要な指標ですが、単独で存在するわけではありません。
LLM APIは、サーバー送信イベント(SSE)またはチャンクHTTP転送エンコーディングを介してトークンを配信します。サーバーは完全な応答をバッファリングするのではなく、生成されたトークン(またはトークンの小さなグループ)をそのままフラッシュします。例えば、OpenAI APIの生のSSEストリームは次のようになります:
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":"The"}}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":" capital"}}]}
data: [DONE]
各data:行は個別のチャンクです。実際のコンテンツを含む最初のdata:行のタイムスタンプがTTFT測定ポイントです。
以下は、PythonのhttpxライブラリとAnthropic Messages APIを使用した正確なTTFT測定の例です:
import httpx
import time
import json
def measure_ttft(prompt: str, api_key: str) -> dict:
url = "https://api.anthropic.com/v1/messages"
headers = {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
}
payload = {
"model": "claude-sonnet-4-20250514",
"max_tokens": 512,
"stream": True,
"messages": [{"role": "user", "content": prompt}],
}
ttft = None
ttlt = None
token_count = 0
request_start = time.perf_counter()
with httpx.Client(timeout=60) as client:
with client.stream("POST", url, headers=headers, json=payload) as response:
for line in response.iter_lines():
if not line.startswith("data:"):
continue
raw = line[len("data:"):].strip()
if raw == "[DONE]":
break
try:
chunk = json.loads(raw)
except json.JSONDecodeError:
continue
event_type = chunk.get("type", "")
if event_type == "content_block_delta":
now = time.perf_counter()
if ttft is None:
ttft = now - request_start
token_count += 1
ttlt = now - request_start
return {
"ttft_ms": round(ttft * 1000, 2) if ttft else None,
"ttlt_ms": round(ttlt * 1000, 2) if ttlt else None,
"token_count": token_count,
"throughput_tokens_per_sec": round(token_count / ttlt, 2) if ttlt else None,
}重要なのは、time.perf_counter()を使用して高解像度のタイミングを取得することです。time.time()はサブ秒精度に適していません。
Node.jsでは、Anthropic SDKのストリーミングインターフェースを使用します:
import Anthropic from "@anthropic-ai/sdk";
interface LLMMetrics {
ttft_ms: number | null;
ttlt_ms: number | null;
token_count: number;
throughput_tokens_per_sec: number | null;
}
async function measureTTFT(prompt: string): Promise<LLMMetrics> {
const client = new Anthropic();
let ttft: number | null = null;
let ttlt: number | null = null;
let tokenCount = 0;
const requestStart = performance.now();
const stream = client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 512,
messages: [{ role: "user", content: prompt }],
});
for await (const chunk of stream) {
if (chunk.type === "content_block_delta") {
const now = performance.now();
if (ttft === null) {
ttft = now - requestStart;
}
tokenCount++;
ttlt = now - requestStart;
}
}
return {
ttft_ms: ttft !== null ? Math.round(ttft * 100) / 100 : null,
ttlt_ms: ttlt !== null ? Math.round(ttlt * 100) / 100 : null,
token_count: tokenCount,
throughput_tokens_per_sec: ttlt && tokenCount ? Math.round((tokenCount / (ttlt / 1000)) * 100) / 100 : null,
};
}performance.now()をDate.now()の代わりに使用し、ミリ秒未満の分解能を得ます。
TTFTのベンチマーク:200ms未満が優秀、200-500msが良好、500-1秒が普通、1-3秒が不良、3秒超はインタラクティブ用途では許容できません。同時負荷下ではこれらの数値は大幅に増加します。したがって、平均ではなくp95 TTFT目標を設定することを推奨します。
一般的な落とし穴:HTTP応答時間をTTFTとして測定する、ストリーミングを有効にしない、低解像度のタイマーを使用する、単一ユーザーの同時実行のみテストする、プロンプト長を変数として無視する、モデルレイテンシとネットワークレイテンシを混同する。