Render無料プランで長時間処理が途中で失敗する?ポーリング処理で確実に完了させる方法

Renderの無料プランでは15分でスリープするため、AI書類作成やデータ分析などの長時間処理が途中で失敗してしまいます。非同期処理+ポーリングに変更することで、スリープ環境でも確実に処理を完了させる方法を解説します。

🚨 こんな問題で困っていませんか?

  • AI書類作成が10分で途中失敗する
  • データ処理や画像変換がタイムアウトで中断
  • 長時間APIが15分スリープでエラーになる
  • 同期処理だとユーザーが長時間待たされる
目次

問題:同期処理だと15分スリープでタスクが失敗する

Renderの制限と長時間処理の相性の悪さを理解しましょう。

Render
Deploy for Free Spin up basic web services and datastores at no charge. Explore new tech, build personal projects, and preview Render's developer experience.

同期処理での問題パターン

処理時間同期処理の結果ユーザー体験
5分以内✅ 正常完了待機時間が長いが成功
10-15分❌ スリープでエラー作業が水の泡
15分超❌ 確実に失敗サービス使用不可

従来の同期処理フロー

// ❌ 問題のある同期処理
app.post('/generate-document', async (req, res) => {
  try {
    // 10分以上かかるAI処理
    const result = await aiService.generateDocument(req.body);
    res.json({ success: true, document: result });
  } catch (error) {
    // 15分でスリープして、ここでエラーになる
    res.status(500).json({ error: 'タイムアウト' });
  }
});

長時間処理の典型例(AI書類作成、データ分析、画像処理など)

Renderでスリープの影響を受けやすい処理パターンを確認しましょう。

スリープ影響を受けやすい処理一覧

処理種別典型的な処理時間具体例
AI文書生成5-20分GPT-4での契約書作成、報告書生成
データ分析10-30分大容量CSVの集計、機械学習モデル実行
画像・動画処理2-15分高解像度画像変換、動画エンコード
外部API連携5-60分複数APIの順次実行、大量データ取得
ファイル処理3-20分PDF結合、Excelレポート生成

実例:AI書類作成での失敗ケース

  • 処理開始:ユーザーが契約書生成をリクエスト
  • 10分経過:AI処理が進行中、ユーザーは待機画面
  • 15分経過:Renderがスリープ、処理が中断
  • 結果:エラーページ表示、生成データも消失

解決策:非同期処理+ポーリングパターンの実装

長時間処理を非同期化し、ポーリングで進捗確認する仕組みを作ります。

非同期+ポーリングの仕組み

  1. 即座にジョブID返却:処理開始と同時にIDを発行
  2. バックグラウンド処理:サーバー側で非同期実行
  3. フロントエンドでポーリング:定期的に進捗確認
  4. 完了時に結果取得:処理完了後にデータ受信

メリット

従来(同期)改善後(非同期+ポーリング)
15分でタイムアウト✅ スリープしても処理継続
ユーザーが固定で待機✅ 進捗表示でUX向上
失敗時は最初からやり直し✅ 途中から再開可能
複数ユーザーで処理できない✅ 並列処理で複数対応

バックエンド:Job Queue APIの作成方法

非同期処理を管理するためのAPI設計と実装方法です。

Express.jsでのJob Queue実装

// jobs.js - ジョブ管理
const jobs = new Map(); // 本番環境ではRedisやDBを使用

// ジョブ作成API
app.post('/api/jobs/document-generate', async (req, res) => {
  const jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  
  // ジョブ情報を保存
  jobs.set(jobId, {
    id: jobId,
    status: 'pending', // pending, processing, completed, failed
    progress: 0,
    createdAt: new Date(),
    result: null,
    error: null
  });
  
  // バックグラウンドで非同期処理開始
  processDocumentGeneration(jobId, req.body);
  
  // 即座にジョブIDを返却
  res.json({ 
    jobId,
    status: 'pending',
    message: '処理を開始しました'
  });
});

// ジョブ状況確認API
app.get('/api/jobs/:jobId', (req, res) => {
  const job = jobs.get(req.params.jobId);
  
  if (!job) {
    return res.status(404).json({ error: 'ジョブが見つかりません' });
  }
  
  res.json(job);
});

// 非同期処理関数
async function processDocumentGeneration(jobId, params) {
  try {
    // ステータス更新
    const job = jobs.get(jobId);
    job.status = 'processing';
    job.progress = 10;
    
    // AI書類作成処理(長時間処理)
    const result = await aiService.generateDocument(params);
    
    // 完了時の更新
    job.status = 'completed';
    job.progress = 100;
    job.result = result;
    job.completedAt = new Date();
    
  } catch (error) {
    // エラー時の更新
    const job = jobs.get(jobId);
    job.status = 'failed';
    job.error = error.message;
  }
}

ジョブステータスの管理

ステータス説明フロントエンド表示
pending処理待ち「処理を開始しています…」
processing実行中「文書を生成中… 45%」
completed完了「生成完了!ダウンロード可能」
failed失敗「エラーが発生しました」

フロントエンド:React/Vueでのポーリング実装

定期的にジョブの進捗を確認し、ユーザーに状況を表示する実装です。

Reactでのポーリング実装

import React, { useState, useEffect } from 'react';

function DocumentGenerator() {
  const [jobId, setJobId] = useState(null);
  const [jobStatus, setJobStatus] = useState(null);
  const [isProcessing, setIsProcessing] = useState(false);

  // 文書生成開始
  const startGeneration = async (formData) => {
    setIsProcessing(true);
    
    try {
      const response = await fetch('/api/jobs/document-generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });
      
      const data = await response.json();
      setJobId(data.jobId);
      
    } catch (error) {
      console.error('ジョブ開始エラー:', error);
      setIsProcessing(false);
    }
  };

  // ポーリング処理
  useEffect(() => {
    if (!jobId || !isProcessing) return;

    const pollJob = async () => {
      try {
        const response = await fetch(`/api/jobs/${jobId}`);
        const job = await response.json();
        
        setJobStatus(job);
        
        // 完了または失敗時にポーリング停止
        if (job.status === 'completed' || job.status === 'failed') {
          setIsProcessing(false);
        }
        
      } catch (error) {
        console.error('ポーリングエラー:', error);
      }
    };

    // 3秒おきにポーリング実行
    const interval = setInterval(pollJob, 3000);
    
    // 初回実行
    pollJob();

    return () => clearInterval(interval);
  }, [jobId, isProcessing]);

  // 進捗表示コンポーネント
  const renderProgress = () => {
    if (!jobStatus) return null;

    switch (jobStatus.status) {
      case 'pending':
        return 
処理を開始しています...
; case 'processing': return (
文書を生成中...
{jobStatus.progress}% 完了
); case 'completed': return (
✅ 生成完了!
); case 'failed': return (
❌ エラーが発生しました: {jobStatus.error}
); default: return null; } }; return (

AI文書生成

{!isProcessing ? ( ) : ( renderProgress() )}
); }

Vue.jsでの実装例

// Vue.js 3 Composition API
import { ref, onUnmounted, watch } from 'vue';

export default {
  setup() {
    const jobId = ref(null);
    const jobStatus = ref(null);
    const isProcessing = ref(false);
    let pollInterval = null;

    const startPolling = () => {
      pollInterval = setInterval(async () => {
        if (!jobId.value) return;
        
        try {
          const response = await fetch(`/api/jobs/${jobId.value}`);
          const job = await response.json();
          jobStatus.value = job;
          
          if (job.status === 'completed' || job.status === 'failed') {
            stopPolling();
          }
        } catch (error) {
          console.error('ポーリングエラー:', error);
        }
      }, 3000);
    };

    const stopPolling = () => {
      if (pollInterval) {
        clearInterval(pollInterval);
        pollInterval = null;
      }
      isProcessing.value = false;
    };

    // ジョブIDが設定されたらポーリング開始
    watch(jobId, (newJobId) => {
      if (newJobId && isProcessing.value) {
        startPolling();
      }
    });

    // コンポーネント破棄時にクリーンアップ
    onUnmounted(() => {
      stopPolling();
    });

    return {
      jobId,
      jobStatus,
      isProcessing,
      startGeneration: async (formData) => {
        isProcessing.value = true;
        const response = await fetch('/api/jobs/document-generate', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(formData)
        });
        const data = await response.json();
        jobId.value = data.jobId;
      }
    };
  }
};

エラーハンドリングとユーザビリティ向上

安定したサービス提供のための重要な考慮事項です。

堅牢なエラーハンドリング

エラーケース対応方法ユーザー表示
ネットワークエラー自動リトライ(3回まで)「接続を再試行中…」
サーバーエラーエラー詳細をログに記録「一時的なエラーです」
ジョブタイムアウト30分でタイムアウト設定「処理時間が上限を超えました」
不正なジョブID404エラーを適切に処理「処理が見つかりません」

UX向上のポイント

  • 進捗の可視化:プログレスバーと%表示
  • 予想時間表示:「あと約5分で完了予定」
  • キャンセル機能:長時間処理の中断オプション
  • 通知機能:完了時のメール通知やブラウザ通知
  • 履歴管理:過去のジョブ結果を保存・再ダウンロード

改善された実装例

// 堅牢なポーリング実装
const useJobPolling = (jobId) => {
  const [jobStatus, setJobStatus] = useState(null);
  const [error, setError] = useState(null);
  const retryCount = useRef(0);

  useEffect(() => {
    if (!jobId) return;

    const pollWithRetry = async () => {
      try {
        const response = await fetch(`/api/jobs/${jobId}`);
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        
        const job = await response.json();
        setJobStatus(job);
        setError(null);
        retryCount.current = 0; // 成功時はリトライカウントリセット
        
        return job.status === 'completed' || job.status === 'failed';
        
      } catch (err) {
        retryCount.current++;
        
        if (retryCount.current >= 3) {
          setError('接続エラーが継続しています。ページを更新してください。');
          return true; // ポーリング停止
        }
        
        setError(`接続エラー (${retryCount.current}/3回目)`);
        return false; // ポーリング継続
      }
    };

    const interval = setInterval(async () => {
      const shouldStop = await pollWithRetry();
      if (shouldStop) {
        clearInterval(interval);
      }
    }, 3000);

    // 初回実行
    pollWithRetry();

    return () => clearInterval(interval);
  }, [jobId]);

  return { jobStatus, error };
};

設定完了!長時間処理も安心して実行

これでRenderのスリープ制限に影響されない、堅牢な長時間処理システムが完成しました。

🎉 解決できたこと

  • スリープ耐性:15分制限を回避して処理継続
  • UX向上:進捗表示でユーザー満足度アップ
  • エラー削減:タイムアウトによる処理失敗を根絶
  • 拡張性:複数ユーザーの同時処理に対応

応用可能な処理例

  • AI・機械学習:文書生成、画像解析、自然言語処理
  • データ処理:大容量CSV分析、レポート生成
  • ファイル処理:動画エンコード、PDF変換
  • 外部API連携:複数サービスとの連携処理

重要なリンク集

非同期処理+ポーリングパターンで、Renderの制限を活かしながら長時間処理を確実に完了させましょう。AI書類作成からデータ分析まで、あらゆる重い処理に応用できる汎用的なソリューションです!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次