NaN/NaN/NaN

月末の請求書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ドキュメントで詳細仕様を確認できます。

関連リンク

Powered by FUNBREW PDF