P99レイテンシは、リクエストの99%がそれ以内に収まる応答時間です。APIのP99が400ミリ秒なら、100回のうち99回は400 ms以内に返り、1回はそれより長くかかります。P95は同じ考え方を95%で見たもので、20回に1回はそれより遅くなります。どちらも分布のテール、つまり平均がならしてしまう遅いリクエストを表します。
テールは、その割合以上に効いてきます。リクエストを1回送るだけのユーザーが気づくことはまれです。音声エージェントと20ターン話す相手は、おそらく気づきます。このガイドでは、パーセンタイルの仕組み、計算方法、数字をごまかしてしまう間違い、そしてリアルタイム音声AIにとっての意味を説明します。
P50・P95・P99の早見表
| パーセンタイル | 意味 | これより遅いリクエスト |
|---|---|---|
| P50 | 中央値。典型的なリクエスト | 2回に1回 |
| P90 | 遅いテールの始まり | 10回に1回 |
| P95 | ダッシュボードやSLOでよく使う目標値 | 20回に1回 |
| P99 | まれな遅延。停止、リトライ、キュー待ち | 100回に1回 |
| P99.9 | 大量のトラフィックでしか見えない外れ値 | 1,000回に1回 |
高いパーセンタイルほど、意味のある値にするには多くのサンプルが必要です。100件のリクエストから出したP99は、たった1件に依存しています。P95には少なくとも数百件、P99には数千件のサンプルを使い、結果の横にサンプル数を書いてください。
平均がテールを隠す理由
100件のリクエストを考えます。94件は200 msで終わり、6件は2秒かかります。平均は308 msで、どのリクエストもその時間はかかっていません。中央値は200 msで、とても良く見えます。P95とP99はどちらも2,000 msで、これが6人のユーザーが実際に体験した時間です。
実際のレイテンシ分布はこのように偏っています。ほとんどのリクエストは速い経路を通り、一部が遅いディスク、コールドキャッシュ、ガベージコレクションの停止、満杯のキューの後ろで待たされます。平均は二つのグループを混ぜ、どちらも表さない数字にします。中央値は遅いグループを完全に無視します。パーセンタイルなら遅いグループが見えたままです。
P95とP99の計算方法
測定値を速い順に並べ、95%または99%の位置にある値を読みます。サンプルが200件なら、P95は190番目、P99は198番目の値です。これが最近接順位法です。
import math
def percentile(samples, p):
"""Nearest rank: the smallest sample with at least p% of samples at or below it."""
ordered = sorted(samples)
rank = math.ceil(p / 100 * len(ordered))
return ordered[max(rank, 1) - 1]
latencies_ms = [200] * 94 + [2000] * 6
print(percentile(latencies_ms, 50)) # 200
print(percentile(latencies_ms, 95)) # 2000
print(percentile(latencies_ms, 99)) # 2000
NumPyは既定で補間するので、numpy.percentileは二つのサンプルの間の値を返すことがあります。最近接順位にするにはmethod="inverted_cdf"を指定します。サンプルが少ないと二つの方法で値がかなり変わることがあるので、その意味でもサンプル数を書いておくべきです。
監視システムは通常、すべてのサンプルを保存しません。リクエストをレイテンシの区間ごとに数え、ヒストグラムからパーセンタイルを推定します。Prometheusのhistogram_quantile()がその例です。推定の精度は周辺の区間の細かさで決まるので、気にしているレイテンシの近くに区間の境界を置いてください。
パーセンタイルを狂わせる三つの間違い
パーセンタイルを平均する。 あるサーバーは900件をすべて200 msで処理し、別のサーバーは100件を処理してそのうち10件に1秒かかったとします。P95は200 msと1,000 msで、平均すると600 msです。しかし1,000件全体の本当のP95は200 msです。Prometheusのドキュメントにも、別々に計算した分位数は集約できないとあります。ヒストグラムか生のサンプルを統合してから計算してください。
段階ごとのパーセンタイルを足す。 音声エージェントは音声認識、言語モデル、音声合成を順に実行します。音声認識が常に300 ms、LLMが通常500 msで10回に1回は1,500 ms、TTSが通常150 msで10回に1回は600 msかかるとします。段階ごとのP95を足すと2,400 msです。しかしターン全体の本当のP95は1,950 msです。二つの遅いケースが同じターンで重なることはまれだからです。部分から推定せず、エンドツーエンドの時間を直接測ってください。
応答を待つクライアントで測る。 前のリクエストが返ってから次を送る負荷生成ツールは、サーバーが遅いときほど送る数が減るので、遅い時間帯が過小評価されます。Gil Teneはこれをcoordinated omissionと呼び、彼の負荷生成ツールwrk2は一定のレートで送ることでこれを避けています。本番の数字には、テスト用クライアントではなく実際のトラフィックを測ってください。
テールが音声エージェントの印象を決める理由
会話のタイミングはシビアです。Levinsonとトレイラ(Torreira)によるターンテイキング研究のレビューでは、ターン間の間はおよそ200 msとされています。Kendrickとトレイラの電話会話の研究では、約700 msを超える間の後の応答は多くが断りなどの好ましくない応答で、約300 msを超える間でも素直な承諾が返りにくくなりました。聞き手は間に意味を読み取るので、900 ms黙る音声エージェントは、相手には遅いだけでなく渋っているように聞こえることがあります。
これをターン数で掛け合わせます。各ターンが独立に5%の確率でP95を超えるなら、20ターンの通話で少なくとも1回超える確率は64%です(1 − 0.95²⁰)。P99でさえ、20ターンの通話の約18%で現れます。中央値は典型的なターンを表し、テールは典型的な通話を表します。GoogleのJeff DeanとLuiz André Barrosoは、大規模サービスについてThe Tail at Scaleで同じことを指摘しています。1回の操作が多数のリクエストに分かれると、まれな遅い応答がありふれたものになります。
音声パイプラインで何を測るか
相手に聞こえるものを測ります。音声エージェントなら、相手が話し終えてから返答の最初の可聴サンプルが届くまでを、相手側の回線で記録します。そのうえで段階ごとに分け、テールの原因を突き止めます。
| 段階 | 測る区間 | テールのよくある原因 |
|---|---|---|
| ターン検出 | 発話の終わりからターン終了の判定まで | 言いよどみ、背景雑音、長い間 |
| 音声認識 | ターン終了から最終的な文字起こしまで | 長いターン、ネットワークのジッター |
| LLMとツール | 文字起こしから最初のトークンまで(ツール含む) | 遅いツール、長いプロンプト、プロバイダーのキュー |
| 音声合成 | 最初のテキスト送信から最初の可聴音声まで | 新しい接続、長い最初の一文 |
| ネットワークと電話 | サーバーを出た音声が相手に届くまで | 通信事業者のルーティング、ジッターバッファ |
注目すべき音声合成の指標は、最初の音声までの時間(TTFA)です。ストリーミングモデルは文が完成する前に再生を始めるからです。先頭の無音も含まれます。300 msの無音で始まるチャンクは、相手にとって300 ms余計な待ち時間です。下のスクリプトは最初のバイトまでを測るので、音声の先頭の無音は別に確認してください。
次のスクリプトは、Cartesiaの音声合成SonicのTTFAパーセンタイルを測ります。0.5秒間隔で205件のリクエストを予定し、最初の5件を除外し、予定時刻から最初の音声バイトまでを計測します。送信時刻ではなく予定時刻から測るので、遅い応答が次のリクエストを遅らせた分も数字に表れ、上で説明したcoordinated omissionを避けられます。
import math
import os
import time
from cartesia import Cartesia
def percentile(samples, p):
"""Nearest rank: the smallest sample with at least p% of samples at or below it."""
ordered = sorted(samples)
rank = math.ceil(p / 100 * len(ordered))
return ordered[max(rank, 1) - 1]
# max_retries=0: a retried request would hide a failure inside a slow sample.
client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"], max_retries=0)
interval = 0.5 # seconds between scheduled requests
ttfa_ms = []
failures = 0
first = time.perf_counter() + interval
for i in range(205):
# Each request has a fixed start time. If a slow response makes us late,
# the wait counts against latency instead of quietly thinning the sample.
scheduled = first + i * interval
time.sleep(max(0.0, scheduled - time.perf_counter()))
got_audio = False
try:
with client.tts.with_streaming_response.generate(
model_id="sonic-3.6",
transcript="Your order shipped this morning and should arrive on Thursday.",
voice="6ccbfb76-1fc6-48f7-b71d-91ac6298247b",
output_format={"container": "raw", "encoding": "pcm_s16le", "sample_rate": 16000},
language="en",
) as response:
for chunk in response.iter_bytes():
if chunk:
got_audio = True
if i >= 5: # the first requests pay for connection setup
ttfa_ms.append((time.perf_counter() - scheduled) * 1000)
break
except Exception:
pass
if not got_audio:
failures += 1
print(f"{len(ttfa_ms)} timed requests, {failures} failed or returned no audio")
if ttfa_ms:
for p in (50, 90, 95, 99):
print(f"p{p}: {percentile(ttfa_ms, p):.0f} ms")
print(f"max: {max(ttfa_ms):.0f} ms")
pip install cartesiaでSDKをインストールし、ダッシュボードで取得したCARTESIA_API_KEYを設定します。サーバーを動かすリージョンから、トラフィックがピークになる時間帯に実行してください。自宅のWi-Fiにつないだノートパソコンで測ると、Wi-Fiを測ることになります。SDKは既定で失敗したリクエストを2回リトライするので、失敗とそのリトライが1件の遅いサンプルに紛れてしまいます。そのためスクリプトではリトライを無効にし、音声が返らなかった応答も含めて失敗を別に数えています。失敗を報告から外さないでください。応答しなかったリクエストこそ最悪のレイテンシです。本番のエージェントでは、ターンをまたいで接続を維持する、実際に使っているWebSocketの経路を測ってください。
公開ベンチマークからわかること
Covalは、音声合成、音声認識、音声対音声の独立したベンチマークを公開APIに対して30分ごとに実行し、中央値、パーセンタイル、サンプル数を公開しています。その方法論では、先頭の無音をTTFAに含め、接続のハンドシェイクは除外しています。
2026年10月15日に確認した30日間の音声合成ランキングでは、各260件以上のサンプルがある33モデルの間で、中央値とP95の差が大きくばらついていました。典型的なモデルでは、P95は中央値の1.38倍でした。両極端を見ると、あるモデルのP95は中央値の1.1倍、別のモデルは7倍以上で、典型が624 ms、20回に1回は4,751 msでした。
教訓はこのガイドの冒頭と同じです。中央値が近い二つのモデルでも、通話での印象は大きく違うことがあります。P95を、サンプル数とともに、ユーザーが体験するのと同じ条件で比べてください。
テールレイテンシを下げる
- 接続を開いたままにする。 HTTPSリクエストを新しく送るたびにTCPとTLSの確立コストがかかり、Cartesiaのドキュメントによれば、このコストは最初の音声バイトまでの時間と同程度になることがよくあります。通話中ずっと開いたWebSocketなら一度で済みます。
- 繰り返すものはキャッシュする。 あいさつ、保留メッセージ、締めの言葉は通話のたびに生成し直す必要はありません。生の音声としてキャッシュし、ライブのストリームに差し込みます。
- 早く話し始める。 段落全体を待たずに、LLMのトークンが届くそばから音声合成に送ります。
- ツールに期限を設ける。 2秒かかるツール呼び出しは、毎回ターンに2秒を足します。タイムアウトを設定し、待つ間はエージェントに何をしているかを話させます。
- 遅いリクエストをヘッジする。 The Tail at Scaleでは、最初のリクエストが通常のP95を超えたら予備のリクエストを送り、先に返った方を使う方法を紹介しています。P95まで待つことで追加の負荷は約5%に抑えられます。同じ音声を作り直すような、二重に送っても安全なリクエストだけに使い、データを書き込むツール呼び出しには使わないでください。
- モデルを通話の近くで動かす。 ネットワークの距離や共有インフラがテールの原因なら、オンプレミス導入でモデルを電話システムのすぐ隣に置けます。
パイプラインを自分で組んで調整したくない場合は、Cartesia Managed Agentsがターン検出、Inkの音声認識、LLM、Sonicを電話番号の上で動かします。プレイグラウンドで作成して電話をかけてみることもできます。自前のパイプラインが遅い場合、直すべき箇所はたいてい測定結果が指す段階にあり、上のスクリプトがその出発点になります。