MENU
AI・機械学習

ストリーミングAIチャットAPIを作ったら429エラーに叩きのめされた話

「とりあえずOpenAI APIを呼び出すだけでチャットbot作れるでしょ?」

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

何が起きたのか?

streaming API error
streaming API error / Photo by Stanislav Kondratiev via Pexels

あるエンジニアが個人サイト向けの簡単なチャット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をそのまま使えるので実装が簡単
  • ✅ ブラウザの EventSource APIで受け取れる
  • ✅ 自動再接続機能がある
  • ❌ クライアント→サーバーの通信には使えない(その場合は通常の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エラーに叩きのめされても、めげずに実装してみましょう! 💪

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

もしも

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

FastAPIでWebAPI開発を実践的に学ぶ

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です