PDF生成 リアルタイム進捗ストリーミング完全ガイド — SSEとX-Job-Idの使い方
ポーリングとWebhookによるPDF生成ステータス追跡を紹介した記事では、非同期PDFジョブのステータスを確認する2つの代表的なアプローチを取り上げました。本記事ではその延長として、第3の選択肢 — Server-Sent Events (SSE) によるリアルタイム進捗ストリーミングを紹介します。
FUNBREW PDF APIは2026年5月31日のアップデートで、バッチジョブと単発生成ジョブの両方に対してSSEエンドポイントを追加しました。進捗バーやステータス表示を画面にリアルタイム反映したい場合に最も適したアーキテクチャです。
ポーリング・Webhook・SSEの3択比較
| ポーリング | Webhook | SSE | |
|---|---|---|---|
| リアルタイム性 | インターバル遅延あり | あり | あり |
| 受信サーバーが必要 | 不要 | 必要 | 不要 |
| ブラウザから直接利用 | 可能 | 不可 | 可能 |
| 余分なAPIコール | あり | なし | なし |
| 中間進捗(%)を取得できる | 不可 | 不可 | 可能 |
| 再接続時の取りこぼし防止 | 別途実装が必要 | 再配信で対応 | Last-Event-ID で自動 |
| 向いているシーン | CLIスクリプト・シンプルなサーバー処理 | サーバー間の確実な通知 | ブラウザUI・進捗表示 |
SSEの最大の利点は「公開HTTPエンドポイントが不要で、ブラウザのネイティブAPIだけで中間進捗を取得できる」点です。WebSocketのような双方向通信は不要で、ブラウザ標準の EventSource のみで接続できます。
SSEとは
Server-Sent Events (SSE) はHTTP上でサーバーからクライアントへのプッシュ配信を行う仕組みです。text/event-stream Content-Typeで接続を維持し、サーバーが随時データを送り込みます。
- HTTP/1.1ベース — WebSocketと異なりプロキシ・Nginxとの相性が良い
- ブラウザ標準 —
EventSourceAPIが全モダンブラウザに内蔵 - 自動再接続 —
EventSourceは切断時に自動で再接続を試みる - 取りこぼし防止 —
Last-Event-IDヘッダで続きから受信できる
FUNBREW PDF APIのSSEエンドポイント
エンドポイント仕様
GET /api/pdf/jobs/{jobId}/events
Accept: text/event-stream
X-API-Key: sk-xxxx
Last-Event-ID: <id> # 任意。前回受信した最後のIDを指定すると続きから受信
jobId は2種類の方法で取得します。
| ジョブの種類 | jobIdの取得方法 |
|---|---|
| バッチ生成 | POST /api/pdf/batch レスポンスの batch_id |
単発生成 (/generate-from-html 等) |
レスポンスヘッダの X-Job-Id、またはリクエスト時に X-Job-Id ヘッダで事前指定 |
認証: 既存の PDF APIキー(generate / download スコープ)をそのまま使用します。Authorization: Bearer sk-xxxx 形式でも受け付けます。
X-Job-Idヘッダの役割
単発生成でSSEを使う場合、X-Job-Id が重要な役割を果たします。
- 事前指定: UUID等を自分で生成し、
X-Job-Idヘッダを付けてジョブを投入すると、SSEを先に開いてから生成リクエストを送ることができる - 事後取得:
X-Job-Idを省いてジョブを投入すると、レスポンスヘッダのX-Job-IdからIDを取得し、そのままSSEを購読できる
イベントの種類とペイロード
SSEストリームは progress イベントと retry ディレクティブを配信します。
retry: 3000
id: 1748649600123-1
event: progress
data: {"phase":"queued","progress":0.0,"message":"enqueued","data":{},"occurred_at":"2026-05-31T01:00:00+00:00"}
id: 1748649600456-2
event: progress
data: {"phase":"rendering","progress":0.5,"message":"rendering pdf","data":{},"occurred_at":"2026-05-31T01:00:01+00:00"}
id: 1748649600789-3
event: progress
data: {"phase":"done","progress":1.0,"message":"batch completed","data":{"batch_id":"...","total_items":3,"completed_items":3,"failed_items":0},"occurred_at":"2026-05-31T01:00:02+00:00"}
フェーズ一覧:
phase |
意味 |
|---|---|
queued |
ジョブ受付完了 |
preprocessing |
HTML検証・フォント解決 |
rendering |
PDFレンダリング中 |
postprocess |
透かし・ページ番号・メタデータ付与 |
uploading |
S3/Wasabiへのアップロード中 |
done |
完了(終端 — サーバーがストリームを閉じる) |
failed |
失敗(終端 — サーバーがストリームを閉じる) |
done または failed を受信するとサーバー側からストリームが閉じられます。クライアント側でも es.close() を呼ぶのが推奨です。
実装例
curlで試す(最も手軽)
# バッチジョブの場合
curl -N \
-H "X-API-Key: $FUNBREW_PDF_API_KEY" \
https://pdf.funbrew.cloud/api/pdf/jobs/<batch_uuid>/events
-N(--no-buffer)は必須です。省くとバッファされてリアルタイムに見えません。
単発生成でSSEを先に開く場合:
# 1. jobIdを事前に生成
JOB_ID=$(uuidgen)
# 2. SSEを先に購読(バックグラウンドで待機)
curl -N -H "X-API-Key: $FUNBREW_PDF_API_KEY" \
"https://pdf.funbrew.cloud/api/pdf/jobs/$JOB_ID/events" &
# 3. 同じjobIdで生成リクエストを投入
curl -X POST https://pdf.funbrew.cloud/api/pdf/generate-from-html \
-H "X-API-Key: $FUNBREW_PDF_API_KEY" \
-H "X-Job-Id: $JOB_ID" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello FUNBREW PDF</h1>"}'
または生成後にレスポンスヘッダからJobIdを取得する方法:
# ヘッダ付きでレスポンスを受け取る
RESP=$(curl -sD - -X POST https://pdf.funbrew.cloud/api/pdf/generate-from-html \
-H "X-API-Key: $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello FUNBREW PDF</h1>"}')
JOB_ID=$(echo "$RESP" | grep -i 'X-Job-Id' | cut -d' ' -f2 | tr -d '\r')
# SSE購読
curl -N -H "X-API-Key: $FUNBREW_PDF_API_KEY" \
"https://pdf.funbrew.cloud/api/pdf/jobs/$JOB_ID/events"
ブラウザ EventSource API
バックエンドAPIキーをブラウザに露出させない形で実装するには、自前のサーバーをプロキシとして経由させるか、APIキーをサーバーサイドで注入する構成をとってください。
const jobId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // バッチIDまたはX-Job-Id
const es = new EventSource(`/api/pdf/jobs/${jobId}/events`, {
withCredentials: false,
});
es.addEventListener('progress', (event) => {
const payload = JSON.parse(event.data);
// 進捗バーを更新
updateProgressBar(payload.progress); // 0.0 〜 1.0
// フェーズに応じてUIを更新
showStatus(payload.phase, payload.message);
// 終端フェーズで購読を終了
if (payload.phase === 'done' || payload.phase === 'failed') {
es.close();
if (payload.phase === 'done') {
console.log('完了:', payload.data);
} else {
console.error('失敗:', payload.message);
}
}
});
es.onerror = (err) => {
// EventSourceは自動再接続する。Last-Event-IDも自動付与されるため、
// 取りこぼしなく続きから受信できる。
console.warn('SSE接続エラー(自動再接続中):', err);
};
Node.js(eventsourceパッケージ)
Node.jsでは EventSource がネイティブに存在しないため、eventsource パッケージを使います。Node.js 22以降はグローバルに EventSource が追加されています。
import EventSource from 'eventsource';
const baseUrl = 'https://pdf.funbrew.cloud';
const apiKey = process.env.FUNBREW_PDF_API_KEY;
const batchUuid = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
const es = new EventSource(`${baseUrl}/api/pdf/jobs/${batchUuid}/events`, {
headers: { 'X-API-Key': apiKey },
});
es.addEventListener('progress', (e) => {
const payload = JSON.parse(e.data);
console.log(`[${payload.phase}] ${Math.round(payload.progress * 100)}% — ${payload.message}`);
if (payload.phase === 'done' || payload.phase === 'failed') {
es.close();
process.exit(payload.phase === 'done' ? 0 : 1);
}
});
@funbrew/pdf-client SDK(推奨)
JS SDK (@funbrew/pdf-client) を使うと、EventSource の手動管理が不要になり、最もシンプルにSSE購読を実装できます。
import { PdfClient } from 'https://pdf.funbrew.cloud/sdk/pdf-client.esm.js';
const client = new PdfClient({
baseUrl: 'https://pdf.funbrew.cloud',
apiKey: 'sk-...', // APIキー
});
// 1. jobIdを先に決める
const jobId = crypto.randomUUID();
// 2. SSE購読を開始
const sub = client.subscribeToJob(jobId, {
onProgress: (event) => {
console.log(`[${event.phase}] ${Math.round(event.progress * 100)}%`);
updateProgressBar(event.progress);
},
onDone: (event) => {
console.log('PDF生成完了:', event.data?.download_url);
sub.close();
},
onError: (err) => {
console.error('SSEエラー:', err);
},
});
// 3. 同じjobIdでジョブを投入(mergeやcompressでも同様)
await client.generateFromHtml(
{ html: '<h1>Hello FUNBREW PDF</h1>', options: { page_size: 'A4' } },
{ jobId }
);
subscribeToJob は内部で EventSource を管理し、Last-Event-ID の付与や接続エラー時の再接続も自動処理します。
フェーズ遷移: queued → preprocessing → rendering → postprocess → uploading → done(失敗時: failed)
ポーリング・Webhook・SSEの使い分けマトリクス
| シナリオ | 推奨アプローチ |
|---|---|
| ブラウザに進捗バーを表示したい | SSE |
| CLIスクリプト・cronジョブ | ポーリング |
| サーバー間の確実な完了通知 | Webhook |
| 受信サーバーが持てない | SSE またはポーリング |
| 長時間バッチ(数分〜数十分) | SSE + Webhook の併用が理想 |
| 短時間ジョブ(5秒以内) | 同期API(ポーリング不要) |
| 高並行(100件以上の同時ジョブ) | Webhook(SSEは接続数制限に注意) |
SSEとWebhookの併用が推奨: SSEは「クライアントが見ている間のUX向上」、Webhookは「最終結果の永続的な伝達」と役割が異なります。どちらか一方に頼るより、UI表示はSSE・バックエンド処理トリガーはWebhookと使い分けるのが本番設計のベストプラクティスです。
注意点と運用ヒント
PHP-FPMのワーカー占有
SSE接続中はPHP-FPMワーカーが1つ専有されます。大量の同時接続が見込まれる場合は次のいずれかを検討してください。
- 専用FPMプールを分ける(SSE接続数分だけワーカーを増やす)
- FrankenPHP / Octane / Swoole に切り替える(1プロセスで数千接続可能)
Nginxの設定
location /api/pdf/jobs/ {
proxy_pass http://app;
proxy_buffering off; # 必須。バッファされるとリアルタイムにならない
proxy_cache off;
proxy_read_timeout 3600s; # 長時間ジョブに対応
proxy_http_version 1.1;
proxy_set_header Connection '';
}
レスポンスヘッダに X-Accel-Buffering: no が付加されているためデフォルトでも動作しますが、proxy_buffering off を明示すると確実です。
タイムアウトと最大接続時間
PDF_PROGRESS_MAX_DURATION(デフォルト: 600秒)を超えると、サーバーがストリームを自動的に閉じます。EventSource の自動再接続と Last-Event-ID により、切断後も取りこぼしなく続きから受信できます。
| 環境変数 | デフォルト | 説明 |
|---|---|---|
PDF_PROGRESS_MAX_DURATION |
600 |
1接続あたりの最大保持時間(秒) |
PDF_PROGRESS_POLL_MS |
500 |
サーバー側のポーリング間隔(ms) |
PDF_PROGRESS_TTL |
86400 |
RedisキーのTTL(秒) |
再接続と取りこぼし防止
Last-Event-ID ヘッダを使うと、切断前に受信した最後のイベントIDからストリームの続きを取得できます。ブラウザの EventSource はこのヘッダを自動で付与します。
IDはRedis StreamのタイムスタンプベースID(<ms>-<seq> 形式)なので、長時間切断されていた場合でも正確に続きから受信できます。
古いIDへの対応
前回の接続から時間が経ちすぎて Last-Event-ID が参照できなくなった場合、サーバーは 410 Gone を返します。この場合は最新のジョブステータスをポーリングで確認するフォールバックを実装してください。
まとめ
FUNBREW PDF APIのSSEストリーミングは、ポーリングとWebhookに続く第3の選択肢として、特にブラウザUIへのリアルタイム進捗表示に適しています。
- ブラウザ標準の
EventSourceのみで利用可能(WebSocket不要) - バッチ・単発ジョブ両方に対応。単発ジョブは
X-Job-Idで事前・事後どちらでも購読可能 Last-Event-IDによる取りこぼし防止 — 再接続時も続きから受信- SDK (
subscribeToJob) で手間なく実装可能
長時間バッチジョブで進捗バーを表示しながら、完了後の処理はWebhookで行う「SSE + Webhookの二刀流」が本番環境での推奨構成です。
関連リンク
- PDF生成ステータス追跡:ポーリングとWebhookの使い分け完全ガイド — ポーリング・Webhookの詳細実装パターン
- JS SDK (@funbrew/pdf-client) 完全ガイド —
subscribeToJobを含むSDK全機能 - 再開可能ファイルアップロード(tus)完全ガイド — 大容量PDFの安定アップロード
- APIドキュメント — エンドポイント仕様・認証・レート制限
- Playground — ブラウザでSSEを試す