「とりあえずOpenAI APIを呼び出すだけでチャットbot作れるでしょ?」
そう思ったことのある方、いませんか? 私も最初まったく同じ気持ちでした。でも現実は甘くなかった……というエピソードが海外の技術コミュニティで話題になっています。今回はその内容をもとに、ストリーミングAIチャットエンドポイントの正しい作り方を一緒に学んでいきましょう! 🚀
何が起きたのか?

あるエンジニアが個人サイト向けの簡単なチャットbotを作ろうとしました。ユーザーが質問を入力 → AIに送る → 回答を表示、というシンプルな構成です。
Node.js + Express でAPIを組んで、OpenAI APIを呼び出すだけ……のはずが、デモ中にチャットbotが何度もフリーズ。そして見慣れない数字が画面に現れます。
429 Too Many Requests
これがいわゆるレートリミット(Rate Limit)エラーです。「一定時間内にAPIを叩きすぎですよ」という警告ですね。
そもそもストリーミングとは?
ChatGPTを使ったことがある方なら、回答がひと文字ずつ順番に表示されていくのを見たことがあると思います。あれがまさにストリーミングです。
イメージとしては——
- ❌ 通常のAPI: 全部の回答が生成されてから「どーん」と一気に返ってくる
- ✅ ストリーミングAPI: 生成された文字をリアルタイムで少しずつ流してくれる
ユーザー体験がぜんぜん違いますよね。待ち時間のストレスが激減します。
実際のコード例を見てみましょう
Node.js(Express)でストリーミングを実装するとこんな感じです。ポイントをまとめるとこんな感じです👇
// ストリーミングAIチャットエンドポイントの例
const express = require('express');
const OpenAI = require('openai');
const app = express();
app.use(express.json());
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
app.post('/chat', async (req, res) => {
const { message } = req.body;
// ストリーミング用のレスポンスヘッダーを設定
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// stream: true でストリーミングモードをON
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || '';
if (text) {
// チャンクをSSE形式でクライアントに送信
res.write(`data: ${JSON.stringify({ text })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (err) {
// 429エラー(レートリミット)を個別にハンドリング
if (err.status === 429) {
res.write('data: ' + JSON.stringify({ error: 'レートリミットに達しました。少し待ってから再試行してください。' }) + '\n\n');
} else {
res.write('data: ' + JSON.stringify({ error: 'エラーが発生しました。' }) + '\n\n');
}
res.end();
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
なぜ429エラーが起きるのか?
そもそも429エラーとは何なのか、もう少し詳しく見てみましょう。
OpenAI APIには「レートリミット」と呼ばれる制限があります。これは「1分間に何リクエストまで」「1分間にトークン数はいくつまで」という上限です。
フリープランや低いティアのAPIキーを使っている場合、この上限が特に低く設定されています。デモ中にポンポンとリクエストを送ったら、あっという間に上限に達してしまうわけです。
| 原因 | 詳細 |
|---|---|
| RPM超過 | 1分間のリクエスト数が多すぎる |
| TPM超過 | 1分間のトークン数が多すぎる |
| 同時接続過多 | 複数ユーザーが同時にAPIを叩いている |
| プランの制限 | 無料・低ティアは上限が厳しい |
429エラーへの正しい対処法
では、429エラーを防いだり、うまく対処したりするにはどうすればよいでしょうか。代表的な対策を3つ紹介します。
① リトライ処理(指数バックオフ)
429が返ってきたら、少し待ってからもう一度リクエストを送る——これがリトライ処理です。待ち時間を毎回2倍にしていく「指数バックオフ」がベストプラクティスとされています。
async function chatWithRetry(message, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
});
return response;
} catch (err) {
if (err.status === 429 && i < retries - 1) {
// 指数バックオフ: 1秒 → 2秒 → 4秒...
const waitTime = Math.pow(2, i) * 1000;
console.log(`レートリミット: ${waitTime}ms 待機してリトライします...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw err;
}
}
}
}
② リクエストキューを使う
複数のリクエストが同時に来た場合は、キュー(待ち行列)に積んで順番に処理する方法が有効です。これにより、一気にAPIを叩くのを防げます。
const queue = [];
let isProcessing = false;
async function enqueue(message) {
return new Promise((resolve, reject) => {
queue.push({ message, resolve, reject });
processQueue();
});
}
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
const { message, resolve, reject } = queue.shift();
try {
const result = await chatWithRetry(message);
resolve(result);
} catch (err) {
reject(err);
} finally {
isProcessing = false;
// 次のリクエストまで少し間隔を空ける
setTimeout(processQueue, 500);
}
}
③ ユーザーへの適切なフィードバック
どれだけ対策しても、429エラーが完全にゼロになるわけではありません。大切なのは、エラーが発生したときにユーザーにわかりやすく伝えることです。
// フロントエンド側でのエラーハンドリング例
const eventSource = new EventSource('/chat');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.error) {
// エラーメッセージをユーザーに表示
showMessage('⚠️ ' + data.error);
return;
}
if (event.data === '[DONE]') {
eventSource.close();
return;
}
appendText(data.text);
};
SSE(Server-Sent Events)って何?
コードの中に text/event-stream というヘッダーが出てきましたが、これはSSE(Server-Sent Events)という仕組みを使っているからです。
SSEは「サーバーからクライアントへの一方向リアルタイム通信」を実現する技術です。WebSocketよりシンプルで、ストリーミングテキストを送るのに最適です。
- ✅ HTTPをそのまま使えるので実装が簡単
- ✅ ブラウザの
EventSourceAPIで受け取れる - ✅ 自動再接続機能がある
- ❌ クライアント→サーバーの通信には使えない(その場合は通常のPOSTを使う)
Python版でも書いてみよう
このブログはPythonがメインなので、FastAPIを使ったPython版も紹介します!
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
import asyncio
import json
app = FastAPI()
client = AsyncOpenAI()
async def stream_chat(message: str):
try:
stream = await client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': message}],
stream=True,
)
async for chunk in stream:
text = chunk.choices[0].delta.content or ''
if text:
yield f'data: {json.dumps({"text": text})}\n\n'
yield 'data: [DONE]\n\n'
except Exception as e:
error_msg = 'レートリミットに達しました。' if '429' in str(e) else 'エラーが発生しました。'
yield f'data: {json.dumps({"error": error_msg})}\n\n'
@app.post('/chat')
async def chat_endpoint(body: dict):
message = body.get('message', '')
return StreamingResponse(
stream_chat(message),
media_type='text/event-stream'
)
FastAPIは非同期処理が得意なので、ストリーミングとの相性がとても良いです。StreamingResponse を使えば、数行でSSEレスポンスを返せます。
実装時のチェックリスト
ストリーミングAIチャットAPIを作る際に確認したいポイントをまとめました。
- ✅
stream: trueを指定してストリーミングモードをON - ✅ レスポンスヘッダーに
Content-Type: text/event-streamを設定 - ✅ 429エラーを個別にキャッチしてリトライ処理を実装
- ✅ 指数バックオフでAPIへの負荷を分散
- ✅ ユーザーにわかりやすいエラーメッセージを表示
- ✅ APIキーは環境変数で管理(ソースコードに直書きしない!)
- ✅ 同時リクエスト数が多い場合はキューを検討
まとめ
「APIを叩くだけでしょ」と思いがちなAIチャットbot開発ですが、実際にはストリーミング・レートリミット・エラーハンドリングなど、考慮すべき点がたくさんあります。
今回学んだポイントを振り返ると——
- 🔹 ストリーミングAPIを使うとユーザー体験が大幅に向上する
- 🔹 429エラーはレートリミット超過のサイン
- 🔹 指数バックオフ+リトライで対応するのがベストプラクティス
- 🔹 SSEは一方向ストリーミングに最適な技術
- 🔹 Python(FastAPI)でも簡単に実装できる
最初は難しく感じるかもしれませんが、一つひとつのエラーと向き合うことで確実にスキルアップできます。429エラーに叩きのめされても、めげずに実装してみましょう! 💪





