月末の請求書100件、研修後の修了証200件、四半期レポート50件――ビジネスではPDFの一括生成が頻繁に必要になります。1件ずつAPIを呼ぶのは遅く、エラーハンドリングも大変です。
この記事では、バッチAPIと並行処理を使って大量のPDFを効率的に生成する方法を、実践的なコード例とともに解説します。FUNBREW PDFのバッチエンドポイントを中心に、Node.js・Python・PHPの並行処理パターンとエラーハンドリングのベストプラクティスを紹介します。
各言語の基本的なAPI呼び出し方法はクイックスタートガイドを参照してください。APIキーの安全な管理についてはセキュリティガイドで解説しています。
一括生成の課題
逐次処理の問題
// NG: 1件ずつ逐次生成 → 100件で数分かかる
for (const customer of customers) {
await generatePdf(customer); // 1件あたり1-3秒
}
// 100件 × 2秒 = 200秒(3分以上)
逐次処理は遅いだけでなく、途中でエラーが発生した場合のリカバリも困難です。顧客への請求書送付が遅れれば、キャッシュフローに直接影響します。
バッチ処理のメリット
- 高速: 複数件を同時に処理(100件を数十秒に短縮)
- 効率的: ネットワークラウンドトリップを削減
- 堅牢: 個別のエラーハンドリングと部分的な成功を管理
- スケーラブル: API側の自動スケーリングを最大限に活用
バッチAPIの使い方
FUNBREW PDFのバッチAPIは、1回のリクエストで複数のPDFを生成できます。
基本的な使い方
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: customers.map(c => ({
type: 'html',
html: buildInvoiceHtml(c),
filename: `invoice-${c.id}.pdf`,
engine: 'quality',
format: 'A4',
})),
}),
});
const { data } = await response.json();
console.log(`${data.results.length}件のPDFを生成しました`);
// 各結果を確認
data.results.forEach(result => {
if (result.status === 'success') {
console.log(`✓ ${result.filename}: ${result.download_url}`);
} else {
console.error(`✗ ${result.filename}: ${result.error}`);
}
});
テンプレートを使ったバッチ生成
テンプレートエンジンと組み合わせると、HTMLを毎回組み立てる必要がなくなります。
const items = customers.map(c => ({
type: 'template',
template: 'invoice',
variables: {
customer_name: c.name,
invoice_number: `INV-${c.id}`,
line_items: buildLineItemsHtml(c.items),
total: c.total.toLocaleString(),
due_date: c.dueDate,
},
filename: `invoice-${c.id}.pdf`,
email: {
to: c.email,
subject: `【請求書】${c.name}様 2026年3月分`,
},
}));
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ items }),
});
PDF生成とメール送信が1リクエストで完了します。請求書自動化の詳細も参考にしてください。
並行処理によるバッチ実装
バッチAPIのサイズ制限がある場合や、自前で並行処理を実装する場合のパターンです。
Node.js: Promise.allSettled
// 最大同時実行数を制限した並行処理
async function generateBatch(items, concurrency = 10) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const chunk = items.slice(i, i + concurrency);
const chunkResults = await Promise.allSettled(
chunk.map(item => generateSinglePdf(item))
);
results.push(...chunkResults);
}
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`完了: ${succeeded}件成功, ${failed}件失敗`);
return results;
}
async function generateSinglePdf(item) {
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: item.html,
engine: 'quality',
format: 'A4',
}),
});
if (!response.ok) throw new Error(`${item.id}: HTTP ${response.status}`);
return { id: item.id, pdf: await response.arrayBuffer() };
}
Python: asyncio + httpx
import asyncio
import httpx
import os
async def generate_batch(items, concurrency=10):
semaphore = asyncio.Semaphore(concurrency)
async with httpx.AsyncClient() as client:
tasks = [generate_one(client, semaphore, item) for item in items]
results = await asyncio.gather(*tasks, return_exceptions=True)
succeeded = sum(1 for r in results if not isinstance(r, Exception))
failed = sum(1 for r in results if isinstance(r, Exception))
print(f"完了: {succeeded}件成功, {failed}件失敗")
return results
async def generate_one(client, semaphore, item):
async with semaphore:
response = await client.post(
'https://api.pdf.funbrew.cloud/v1/pdf/from-html',
headers={'Authorization': f'Bearer {os.environ["FUNBREW_PDF_API_KEY"]}'},
json={'html': item['html'], 'engine': 'quality', 'format': 'A4'},
timeout=30,
)
response.raise_for_status()
return {'id': item['id'], 'pdf': response.content}
asyncio.run(generate_batch(items))
PHP (Laravel): Job Queue
大量のPDFをLaravelのキューで処理する方法です。
// GeneratePdfJob.php
class GeneratePdfJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private Customer $customer,
private string $month,
) {}
public function handle(): void
{
$response = Http::withToken(config('services.funbrew.api_key'))
->post('https://api.pdf.funbrew.cloud/v1/pdf/from-template', [
'template' => 'invoice',
'variables' => [
'customer_name' => $this->customer->name,
'total' => number_format($this->customer->total),
],
'email' => [
'to' => $this->customer->email,
'subject' => "【請求書】{$this->customer->name}様 {$this->month}分",
],
]);
if ($response->failed()) {
throw new \Exception("PDF generation failed: {$response->status()}");
}
}
public int $tries = 3;
public int $backoff = 60;
}
// ディスパッチ
Customer::chunk(100, function ($customers) {
foreach ($customers as $customer) {
GeneratePdfJob::dispatch($customer, '2026年3月');
}
});
キューを使えば、リトライ、遅延実行、優先度制御が自動化されます。
エラーハンドリング
バッチ処理では部分的な失敗が発生します。適切なエラーハンドリングが重要です。
リトライ戦略
async function generateWithRetry(item, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await generateSinglePdf(item);
} catch (error) {
if (attempt === maxRetries) throw error;
// 指数バックオフ: 1秒、2秒、4秒...
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
}
}
}
失敗件数の監視
大量生成時は失敗率を監視し、閾値を超えたら処理を停止するのが安全です。
let failCount = 0;
const failThreshold = items.length * 0.1; // 10%以上失敗で停止
for (const chunk of chunks) {
const results = await Promise.allSettled(chunk.map(generateSinglePdf));
failCount += results.filter(r => r.status === 'rejected').length;
if (failCount > failThreshold) {
console.error(`失敗率が閾値を超えました(${failCount}件)。処理を停止します。`);
break;
}
}
Webhookによる完了通知
バッチ処理の完了をWebhookで通知すれば、ポーリングが不要になります。
{
"event": "batch.completed",
"data": {
"batch_id": "batch_abc123",
"total": 100,
"succeeded": 98,
"failed": 2,
"failed_items": [
{"filename": "invoice-42.pdf", "error": "Template variable missing"}
]
}
}
パフォーマンス最適化のコツ
| テクニック | 効果 | 実装難易度 |
|---|---|---|
fastエンジンを使う |
生成速度2-3倍 | 低 |
| テンプレートを使う | HTML転送量の削減 | 低 |
| 並行数を最適化する | スループット向上 | 中 |
| 画像をBase64からURLに変更 | ペイロードサイズ削減 | 中 |
| バッチサイズを調整する | メモリ効率の改善 | 中 |
エンジンの特性についてはwkhtmltopdf vs Chromiumを参照してください。デザインが重要でなければfastエンジンで十分なケースが多いです。
ベンチマーク:逐次 vs バッチ vs 並行処理
実際の生成速度を計測した場合の目安です(環境・コンテンツによって異なります)。
| 処理方式 | 100件の処理時間 | 500件の処理時間 | 適した規模 |
|---|---|---|---|
| 逐次(1件ずつ) | 約200秒(3分) | 約1,000秒(17分) | 〜10件/日 |
| バッチAPI | 約20〜40秒 | 約100〜200秒 | 〜10,000件/日 |
| 並行処理(concurrency=10) | 約25〜50秒 | 約125〜250秒 | 〜100,000件/日 |
| バッチ + 並行(最適化) | 約10〜20秒 | 約50〜100秒 | 〜1,000,000件/日 |
バッチAPIを使うだけで逐次比5〜10倍のスループットが得られます。さらに並行処理と組み合わせると、大規模なユースケースにも対応できます。
エンジン選択によるパフォーマンス差
// fastエンジン vs qualityエンジンの使い分け
const configs = {
// 大量バッチ向け(速度優先)
bulkInvoice: {
engine: 'fast',
format: 'A4',
// 生成速度: qualityの約2〜3倍
},
// 重要文書向け(品質優先)
contract: {
engine: 'quality',
format: 'A4',
// Chromiumによる高精度レンダリング
},
};
wkhtmltopdf vs Chromiumでエンジンの詳細な特性を解説しています。
ユースケース別の実装パターン
月次請求書の自動生成
最も一般的なバッチ処理のユースケースです。月末に全顧客分の請求書を一括生成します。
import asyncio
import httpx
from datetime import date
async def generate_monthly_invoices(customers, month):
"""月次請求書の一括生成"""
items = [
{
'type': 'template',
'template': 'invoice',
'variables': {
'customer_name': c['name'],
'invoice_number': f"INV-{month}-{c['id']:04d}",
'amount': f"{c['total']:,}",
'due_date': c['due_date'],
},
'filename': f"invoice-{c['id']}-{month}.pdf",
'email': {
'to': c['email'],
'subject': f"【請求書】{c['name']}様 {month}分",
},
}
for c in customers
]
async with httpx.AsyncClient() as client:
response = await client.post(
'https://pdf.funbrew.cloud/api/v1/batch',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'items': items},
timeout=300, # バッチは長めのタイムアウト
)
result = response.json()
print(f"完了: {result['data']['succeeded']}件成功, {result['data']['failed']}件失敗")
return result
# 実行
asyncio.run(generate_monthly_invoices(customers, '2026年4月'))
詳細な請求書テンプレート設計は請求書PDF自動生成ガイドを参照してください。
研修修了証の大量発行
eラーニングや研修プラットフォームで受講完了時に修了証を発行するパターンです。
// 研修修了者リストから証明書を一括生成
async function generateCertificates(completedLearners) {
const today = new Date().toLocaleDateString('ja-JP', {
year: 'numeric', month: 'long', day: 'numeric'
});
const response = await fetch('https://pdf.funbrew.cloud/api/v1/batch', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
body: JSON.stringify({
items: completedLearners.map(learner => ({
type: 'template',
template: 'certificate',
variables: {
learner_name: learner.name,
course_name: learner.course,
completion_date: today,
certificate_id: `CERT-${learner.id}`,
},
filename: `certificate-${learner.id}.pdf`,
engine: 'quality', // 証明書は品質優先
})),
}),
});
return response.json();
}
証明書の詳細設計についてはPDF証明書自動生成ガイドで解説しています。
定期レポートの自動配信
週次・月次のビジネスレポートを関係者全員に自動配信するパターンです。
// Laravel: 週次レポートのスケジュール実行
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->job(new GenerateWeeklyReports)->weeklyOn(1, '07:00');
}
// app/Jobs/GenerateWeeklyReports.php
class GenerateWeeklyReports implements ShouldQueue
{
public function handle(): void
{
$managers = User::role('manager')->get();
$items = $managers->map(function ($manager) {
return [
'type' => 'template',
'template' => 'weekly_report',
'variables' => [
'manager_name' => $manager->name,
'period' => now()->startOfWeek()->format('Y年m月d日')
. '〜' . now()->endOfWeek()->format('m月d日'),
'kpi_data' => $this->buildKpiData($manager->team_id),
],
'filename' => "weekly-report-{$manager->id}.pdf",
'email' => [
'to' => $manager->email,
'subject' => '週次レポート: ' . now()->format('Y年m月d日'),
],
];
})->toArray();
Http::withToken(config('funbrew.api_key'))
->timeout(300)
->post('https://pdf.funbrew.cloud/api/v1/batch', compact('items'));
}
}
レポートPDFの詳細な設計はビジネスレポートPDF自動生成ガイドを参照してください。
バッチ処理のトラブルシューティング
タイムアウトが発生する
バッチAPIのリクエストタイムアウトは、生成件数に応じて設定してください。
// 件数に応じたタイムアウト設定
function getTimeout(itemCount) {
// 1件あたり3秒 + バッファ30秒
return itemCount * 3000 + 30000;
}
const response = await fetch('https://pdf.funbrew.cloud/api/v1/batch', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
body: JSON.stringify({ items }),
signal: AbortSignal.timeout(getTimeout(items.length)),
});
一部のPDFが生成されない
バッチAPIはパーシャル成功(一部成功・一部失敗)に対応しています。失敗した項目を抽出してリトライしてください。
result = await generate_batch(items)
failed_items = [
items[i]
for i, r in enumerate(result['data']['results'])
if r['status'] == 'failed'
]
if failed_items:
# 失敗した項目のみを再実行
retry_result = await generate_batch(failed_items)
メモリ不足エラー
一度に大量のHTMLを送信する場合、リクエストサイズが制限を超えることがあります。テンプレートを使えばHTMLの転送量を大幅に削減できます。
// NG: 毎回完全なHTMLを送信(大きなペイロード)
items.map(c => ({
type: 'html',
html: buildFullInvoiceHtml(c), // 数KB〜数十KB
}))
// OK: テンプレートIDと変数のみ送信(小さなペイロード)
items.map(c => ({
type: 'template',
template: 'invoice',
variables: { name: c.name, total: c.total }, // 数百バイト
}))
まとめ
大量PDF生成を効率的に処理するポイント:
- バッチAPI: 1リクエストで複数PDF生成、メール送信まで一括
- 並行処理:
Promise.allSettledやasyncioで同時実行数を制御 - キュー: Laravel等のジョブキューでリトライ・遅延実行を自動化
- エラーハンドリング: リトライ戦略と失敗率監視で堅牢に
- Webhook通知: ポーリング不要でバッチ完了を検知
- テンプレート活用: HTMLをテンプレート化してペイロードを削減
まずは無料プラン(月30件)でバッチAPIを試してみてください。Playgroundで動作確認し、APIドキュメントで詳細仕様を確認できます。
関連リンク
- PDF API本番運用チェックリスト — バッチ処理を含む本番運用の全体像
- 請求書PDFの自動生成 — 月次請求のバッチ処理実例
- PDF生成APIクイックスタート — 各言語の基本的なAPI呼び出し
- Webhook連携ガイド — 完了通知の設定方法
- PDF APIセキュリティガイド — 大量生成時のセキュリティ対策
- wkhtmltopdf vs Chromium — エンジン選択によるパフォーマンス最適化
- ビジネスレポートPDF自動生成 — KPIダッシュボード・月次レポートのバッチ自動化実例