<p>EC事業者からの相談で最も解決を急ぐカテゴリのひとつが、決済プロセッサのタイムアウトです。注文確定のボタンを押してから60秒、ブラウザがぐるぐる回り続け、その後「エラーが発生しました」とだけ表示される。ユーザは戸惑い、もう一度カートに戻って購入し直す。あとから確認すると、決済プロセッサ側では一度目も二度目も課金が成立しており、顧客は二重課金された状態で泣きついてくる。</p>
<p>原因を一箇所に絞ることはまずできません。プロセッサ側のAPIレイテンシ、加盟店サーバーから決済APIへのネットワーク経路、3-Dセキュア2.0のチャレンジフロー、Webhookの取りこぼし、データベース側のロック競合。複数の層が同時に問題を抱えていることが大半です。</p>
<p><strong>最初に開くべき4つの数値</strong></p>
<p>Stripeを使っているなら、Dashboardの「開発者→ログ」から、直近24時間のAPI応答時間を見ます。正常時の<code>p99</code>は400〜600ms。これが2,000msを継続して超えていれば、Stripe側ではなく自社の呼び出し方を疑います。<code>charges.create</code>を同期的に叩いて待つ実装は、3-Dセキュアのチャレンジ画面表示まで含めると15秒以上待たされることがあり、ALBやCloudflareの30秒タイムアウトに引っかかります。</p>
<p>GMOペイメントゲートウェイは管理画面のエラーログから、エラーコード<code>E11</code>(通信エラー)、<code>E14</code>(タイムアウト)、<code>E91</code>(カード会社リジェクト)の発生比率を確認します。E14が全体決済の0.5%を超えていたら異常事態です。GMOは旧APIと新APIで挙動が違い、旧API(受注ID連携方式)はリトライ仕様がドキュメントに明記されておらず、二重課金が起きやすい構造になっています。</p>
<p>Squareは開発者ダッシュボードのWebhookセクションでイベント配信の失敗率を見ます。失敗率が1%超で特定時間帯に集中していれば、自社のWebhook受信エンドポイントが詰まっています。<code>X-Square-Hmacsha256-Signature</code>の検証処理が同期DB書き込みと一緒になっていて、200を返すまでに10秒以上かかっているケースが頻出します。</p>
<p>4つ目は加盟店サーバー側のアクセスログです。Nginxの<code>$request_time</code>を決済エンドポイントだけ抽出して、awkで99パーセンタイルを出すとユーザ視点の実態が見えます。プロセッサ側が500msでも、自社のチェックアウト処理全体で12秒かかっていれば、決済はプロセッサのせいではありません。</p>
<p><strong>二重課金が起きる典型パターン</strong></p>
<p>タイムアウトの本当の怖さは、決済が成立しているかどうか不明なまま、ユーザが「もう一度購入」を押してしまうことです。EC-CUBEやWooCommerceのデフォルト実装は決済APIを同期で呼び出し、レスポンスを待ってから注文を確定させます。タイムアウト時にトランザクションをロールバックする実装はあっても、プロセッサ側で課金が成立しているかを確認しに行く処理は組み込まれていません。</p>
<p>正しくは、決済API呼び出しに冪等性キー(idempotency key)を付与し、タイムアウト後に同じキーで再問い合わせします。StripeはHTTPヘッダの<code>Idempotency-Key</code>、GMOペイメントは受注IDによる重複排除(ただし旧API/新APIで仕様が異なる)、SquareはAPIパラメータの<code>idempotency_key</code>。これを実装していないシステムは、流量が増えるとほぼ確実に二重課金が出ます。</p>
<p>もう一つ厄介なのが、3-Dセキュア認証で別タブが開いた時の挙動です。Stripeの<code>PaymentIntent</code>でも、<code>requires_action</code>状態を経由してチャレンジが入ると、ユーザがチャレンジ画面で5分以上止まることがあります。その間、加盟店サーバ側のセッションがタイムアウトすると、戻ってきたコールバックが宙に浮きます。Webhookで<code>payment_intent.succeeded</code>を別経路で受けて注文を確定する設計でないと、決済が成立しているのに注文未確定で残るケースが日常的に起きます。</p>
<p><strong>自社で出来ること、出来ないこと</strong></p>
<p>ネットワーク経路の問題は<code>mtr</code>(tracerouteの継続版)で診断できます。日本国内のEC2インスタンス(ap-northeast-1)からStripe APIへのパケットロス率を30分間記録し、0.1%以上のロスが見えればAWS Direct ConnectやIIJの閉域回線が選択肢に入ります。ただし大半のケースで、ネットワークではなく自社サーバ側のPHP-FPMのワーカー枯渇が原因です。<code>pm.max_children</code>を10のまま運用していて、ピーク時にワーカーが空かず後続リクエストが待たされている、というのが典型的です。</p>
<p>一方で、3-Dセキュア2.0のチャレンジフロー設計、Webhookの取りこぼし対策、カード会社側のリトライ仕様の解釈は、決済プロセッサのドキュメントを精読しないと判断できません。Stripe Radarのスコアリングが過敏になり、特定カードBINだけ拒否しているケースもあります。</p>
<p>二重課金は信用に直結します。SNSで1件騒がれれば、その月の新規流入の半分が失われると考えるのが現実的です。タイムアウトが日に複数回出ているなら、自社で対処を続けず、決済まわりに実装経験のある外部を一度入れた方が安全です。止血と恒久対策は別の話で、まず止血を優先します。</p>
<p>原因を一箇所に絞ることはまずできません。プロセッサ側のAPIレイテンシ、加盟店サーバーから決済APIへのネットワーク経路、3-Dセキュア2.0のチャレンジフロー、Webhookの取りこぼし、データベース側のロック競合。複数の層が同時に問題を抱えていることが大半です。</p>
<p><strong>最初に開くべき4つの数値</strong></p>
<p>Stripeを使っているなら、Dashboardの「開発者→ログ」から、直近24時間のAPI応答時間を見ます。正常時の<code>p99</code>は400〜600ms。これが2,000msを継続して超えていれば、Stripe側ではなく自社の呼び出し方を疑います。<code>charges.create</code>を同期的に叩いて待つ実装は、3-Dセキュアのチャレンジ画面表示まで含めると15秒以上待たされることがあり、ALBやCloudflareの30秒タイムアウトに引っかかります。</p>
<p>GMOペイメントゲートウェイは管理画面のエラーログから、エラーコード<code>E11</code>(通信エラー)、<code>E14</code>(タイムアウト)、<code>E91</code>(カード会社リジェクト)の発生比率を確認します。E14が全体決済の0.5%を超えていたら異常事態です。GMOは旧APIと新APIで挙動が違い、旧API(受注ID連携方式)はリトライ仕様がドキュメントに明記されておらず、二重課金が起きやすい構造になっています。</p>
<p>Squareは開発者ダッシュボードのWebhookセクションでイベント配信の失敗率を見ます。失敗率が1%超で特定時間帯に集中していれば、自社のWebhook受信エンドポイントが詰まっています。<code>X-Square-Hmacsha256-Signature</code>の検証処理が同期DB書き込みと一緒になっていて、200を返すまでに10秒以上かかっているケースが頻出します。</p>
<p>4つ目は加盟店サーバー側のアクセスログです。Nginxの<code>$request_time</code>を決済エンドポイントだけ抽出して、awkで99パーセンタイルを出すとユーザ視点の実態が見えます。プロセッサ側が500msでも、自社のチェックアウト処理全体で12秒かかっていれば、決済はプロセッサのせいではありません。</p>
<p><strong>二重課金が起きる典型パターン</strong></p>
<p>タイムアウトの本当の怖さは、決済が成立しているかどうか不明なまま、ユーザが「もう一度購入」を押してしまうことです。EC-CUBEやWooCommerceのデフォルト実装は決済APIを同期で呼び出し、レスポンスを待ってから注文を確定させます。タイムアウト時にトランザクションをロールバックする実装はあっても、プロセッサ側で課金が成立しているかを確認しに行く処理は組み込まれていません。</p>
<p>正しくは、決済API呼び出しに冪等性キー(idempotency key)を付与し、タイムアウト後に同じキーで再問い合わせします。StripeはHTTPヘッダの<code>Idempotency-Key</code>、GMOペイメントは受注IDによる重複排除(ただし旧API/新APIで仕様が異なる)、SquareはAPIパラメータの<code>idempotency_key</code>。これを実装していないシステムは、流量が増えるとほぼ確実に二重課金が出ます。</p>
<p>もう一つ厄介なのが、3-Dセキュア認証で別タブが開いた時の挙動です。Stripeの<code>PaymentIntent</code>でも、<code>requires_action</code>状態を経由してチャレンジが入ると、ユーザがチャレンジ画面で5分以上止まることがあります。その間、加盟店サーバ側のセッションがタイムアウトすると、戻ってきたコールバックが宙に浮きます。Webhookで<code>payment_intent.succeeded</code>を別経路で受けて注文を確定する設計でないと、決済が成立しているのに注文未確定で残るケースが日常的に起きます。</p>
<p><strong>自社で出来ること、出来ないこと</strong></p>
<p>ネットワーク経路の問題は<code>mtr</code>(tracerouteの継続版)で診断できます。日本国内のEC2インスタンス(ap-northeast-1)からStripe APIへのパケットロス率を30分間記録し、0.1%以上のロスが見えればAWS Direct ConnectやIIJの閉域回線が選択肢に入ります。ただし大半のケースで、ネットワークではなく自社サーバ側のPHP-FPMのワーカー枯渇が原因です。<code>pm.max_children</code>を10のまま運用していて、ピーク時にワーカーが空かず後続リクエストが待たされている、というのが典型的です。</p>
<p>一方で、3-Dセキュア2.0のチャレンジフロー設計、Webhookの取りこぼし対策、カード会社側のリトライ仕様の解釈は、決済プロセッサのドキュメントを精読しないと判断できません。Stripe Radarのスコアリングが過敏になり、特定カードBINだけ拒否しているケースもあります。</p>
<p>二重課金は信用に直結します。SNSで1件騒がれれば、その月の新規流入の半分が失われると考えるのが現実的です。タイムアウトが日に複数回出ているなら、自社で対処を続けず、決済まわりに実装経験のある外部を一度入れた方が安全です。止血と恒久対策は別の話で、まず止血を優先します。</p>
この症状でお困りなら、まず無料相談
60分無料相談を予約