flash-sale-scaling について
フラッシュセールや急な需要増に備えるECチーム向けのスキルです。プラットフォーム別の準備に加え、事前スケーリング、注文キュー、Redisによる原子的な在庫予約、サーキットブレーカーを扱います。
主な機能
- 01プラットフォーム別のセール準備
- 02インフラの事前ウォームアップと拡張
- 03キューによる注文受付
- 04Redisによる原子的な在庫予約
- 05サーキットブレーカーと待合室
活用シーン
- 01新商品の発売に向けたインフラ準備
- 02トラフィック急増時の売り越し抑制
- 03大型プロモーションの容量計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill flash-sale-scalingインストール後、Claude Code で/flash-sale-scalingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/circuit-breaker-metrics-and-db-read-rout/criteria.jsontask.mdevals/redis-atomic-inventory-and-waiting-room/criteria.jsontask.mdevals/sqs-queue-checkout-hpa-pre-warming-artil/criteria.jsontask.md詳細
フラッシュセールのスケーリング
概要
フラッシュセールや新商品の限定販売では、通常の50~100倍のトラフィックがセール開始から数秒以内に集中します。事前の準備がなければ、チェックアウトサービスがダウンし、在庫数を超えて販売され、お客様にはエラーページが表示されます。このスキルでは、極端なトラフィックに対処するために必要なインフラストラクチャのパターンとして、事前のウォームアップ、バックプレッシャーを伴うキューベースの注文受付、Redis ベースのアトミックな在庫予約、負荷時に適切に機能を縮退させるサーキットブレーカーについて説明します。
このスキルを使用するタイミング
- フラッシュセール、数量限定商品の販売、大規模なプロモーションイベントを計画するとき
- 過去の販売でチェックアウトのタイムアウト、在庫数を超えた販売、データベース障害が発生したとき
- ブラックフライデー/サイバーマンデーの計画が進んでおり、インフラストラクチャの見直しが必要なとき
- 新商品の発表によって、突然の高トラフィックが見込まれるとき
主な指示
ステップ 1:利用しているプラットフォームと、制御できる範囲を確認する
| プラットフォーム | フラッシュセールのスケーリング戦略 | 主な対応 |
|---|---|---|
| Shopify | Shopify が自動的にスケーリングするため、インフラストラクチャに関する作業は不要です | テーマの速度を重視します(ページをキャッシュし、画像を最適化する)。需要の高い販売開始時には Shopify のキューページを有効にし、Launchpad(Shopify Plus)を使ってセールをスケジュールし、自動化します |
| WooCommerce | サーバーを自分で管理するため、大幅な事前準備が必要です | スケーラブルなホスティングサービス(Cloudways、Kinsta、WP Engine)にアップグレードし、Redis Object Cache と WP Rocket のページキャッシュを有効にして、Cloudflare を設定します。1~2週間前に負荷テストを実施します |
| BigCommerce | BigCommerce が自動的にスケーリングします | BigCommerce のフラッシュセール機能(プレビュー:推定待ち時間を表示)を使用し、カタログの準備状況とテーマのパフォーマンスを重視します |
| カスタム/ヘッドレス | インフラストラクチャ全体を自分で制御する必要があります | 以下のすべてのパターンを適用します:事前のスケーリング、Redis による在庫管理、キューベースのチェックアウト、サーキットブレーカー |
ステップ 2:プラットフォーム別のフラッシュセール準備
Shopify
Shopify は自動的にスケーリングするため、事実上あらゆるトラフィックの急増に対応できます。必要な準備は次のとおりです。
-
Shopify の需要が高い場合のチェックアウトキューを有効にする(Shopify Plus):
- オンラインストア → 設定 → チェックアウトに移動します
- チェックアウトの同時実行数を有効にします。これにより、トラフィックの多い限定販売向けの Shopify の仮想待合室が有効になります
- 在庫数限定の商品(新商品の限定販売)では、組み込みの在庫予約機能を使用すると、チェックアウトに進んだお客様の商品が10分間確保されます
-
セールのスケジュール設定に Launchpad(Shopify Plus)を使用する:
- Shopify App Store からLaunchpadをインストールします(Plus のマーチャントは無料)
- セールの開始/終了時刻、価格変更、在庫の提供状況を事前にスケジュールします
- Launchpad はスケジュールされた時刻にアトミックに有効化するため、負荷がかかっている間の手動での価格変更を避けられます
-
ストアのパフォーマンスを事前にテストする(すべての Shopify プラン):
- オンラインストア → テーマに移動し、レポートを表示をクリックして、ストアの Core Web Vitals を確認します
- 最も重要なページ(商品ページ、コレクションページ、チェックアウト)で Google PageSpeed Insights を実行します
- セール前に、赤色またはオレンジ色の問題を修正します。最も一般的な修正は画像の圧縮です
-
大規模な販売開始前に Shopify サポートに連絡する(Shopify Plus):
- Plus のサポートチャネルからフラッシュセール通知を送信します。Shopify はイベント中にリソースを事前に割り当て、ストアを監視できます
WooCommerce
WooCommerce では、アクセスが集中するイベントの前に、インフラストラクチャの大幅な整備が必要です。
ホスティングのアップグレード(最も重要):
- ホスティングプランがスケールできることを確認します。共有ホスティングではなく、ワンクリックで水平スケーリングできる Cloudways、自動スケーリングに対応した Kinsta、または自動スケーリングのアドオンがある WP Engine を使用します
- 共有ホスティングを利用している場合は、安定稼働のための時間を確保できるよう、セールの少なくとも1週間前に VPS またはマネージド WordPress ホスティングへ移行します
- Cloudways の場合は、イベント前に Servers → [server] → Vertical Scaling を開き、より大きなサーバーサイズを選択します。終了後に元のサイズに戻します
キャッシュ構成:
- WP Rocket(ページキャッシュ)と Redis Object Cache(データベースクエリキャッシュ)をインストールして設定します
- WP Rocket では、セール開始前にページをウォームアップできるよう、Preload cache を有効にします
- Redis Object Cache では、Redis が有効になっていることを確認します(Settings → Redis でステータスが緑色)
- Cloudflare を有効にして、Caching level を Standard に設定します。商品ページとショップページについて、ストアの URL を Cloudflare Page Rules に追加し、Cache Everything を設定します
在庫の過剰販売防止:
- WooCommerce 組み込みの在庫管理を有効にします。WooCommerce → Settings → Products → Inventory を開き、Enable Stock Management にチェックを入れます
- Hold stock(分)を60に設定します。これにより、商品は顧客のカート内で60分間確保され、その後在庫に戻されます
- 数量限定の商品では、在庫がちょうど0になった時点で売り切れになるよう、Allow backorders を Do not allow に設定します
セール前の負荷テスト:
- Loader.io(無料プラン:1ターゲット、10K接続)を使い、ステージングサイトに対して想定されるピーク時のトラフィックをシミュレートします
- 特にチェックアウトのフローをテストします。商品閲覧は通常キャッシュされますが、チェックアウトではデータベースにアクセスするためです
- セール当日までに、失敗や応答の遅延を解消します
Custom / Headless
インフラストラクチャの事前ウォームアップ(セール開始30分前に実行):
# Kubernetes: scale checkout deployment up before sale
kubectl scale deployment checkout-service --replicas=50
# Or schedule automatic scaling with a CronJob
# See the CronJob example below
Redis によるアトミックな在庫予約(過剰販売を防止):
// lib/inventory.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Initialize inventory in Redis before the sale
export async function initializeInventory(productId, quantity) {
await redis.set(`inventory:${productId}`, quantity);
}
// Atomic check-and-decrement using Lua script (runs on Redis server, no race conditions)
const LUA_RESERVE = `
local current = tonumber(redis.call('GET', KEYS[1]))
if current == nil then return -1 end
if current < tonumber(ARGV[1]) then return 0 end
redis.call('DECRBY', KEYS[1], tonumber(ARGV[1]))
return 1
`;
export async function reserveInventory(productId, quantity) {
const result = await redis.eval(LUA_RESERVE, 1, `inventory:${productId}`, quantity);
if (result === 1) return 'reserved';
if (result === 0) return 'out_of_stock';
return 'not_found';
}
export async function releaseInventory(productId, quantity) {
await redis.incrby(`inventory:${productId}`, quantity);
}
キューベースの注文受付(高速応答、非同期処理):
// checkout API — responds instantly, processes in background
export async function POST(req) {
const order = await req.json();
// 1. Reserve inventory atomically
const reservation = await reserveInventory(order.productId, order.quantity);
if (reservation === 'out_of_stock') {
return Response.json({ error: 'Sold out' }, { status: 409 });
}
// 2. Enqueue — responds to user in <100ms
const orderId = crypto.randomUUID();
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.ORDER_QUEUE_URL,
MessageBody: JSON.stringify({ orderId, ...order }),
}));
return Response.json({
orderId,
status: 'queued',
message: 'Your order is being processed. You will receive a confirmation email shortly.',
});
}
// Background order processor (separate service consuming SQS)
export async function processOrder(message) {
const order = JSON.parse(message.Body);
try {
await capturePayment(order);
await db.orders.create(order);
await sendOrderConfirmationEmail(order);
} catch (err) {
await releaseInventory(order.productId, order.quantity); // release on failure
throw err; // let SQS retry
}
}
決済プロセッサー向けサーキットブレーカー:
import CircuitBreaker from 'opossum';
const paymentBreaker = new CircuitBreaker(captureStripePayment, {
timeout: 5000, // 5s timeout per call
errorThresholdPercentage: 30, // open if 30% fail
resetTimeout: 30000, // try again after 30s
});
// Fallback: queue for retry instead of failing the customer
paymentBreaker.fallback(async (order) => {
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.PAYMENT_RETRY_QUEUE_URL,
MessageBody: JSON.stringify(order),
DelaySeconds: 30,
}));
return { status: 'payment_queued' };
});
Kubernetes の事前スケーリング用 CronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: flash-sale-prescale
spec:
schedule: "30 11 * * 5" # 30 min before noon Friday sale
jobTemplate:
spec:
template:
spec:
containers:
- name: scaler
image: bitnami/kubectl
command: [kubectl, scale, deployment/checkout-service, --replicas=100]
restartPolicy: OnFailure
非常に需要の高いドロップ向け Redis 待合室:
// Fair queue: customers get a position number when they arrive
export async function enterWaitingRoom(sessionId, productId) {
const queueKey = `sale_queue:${productId}`;
await redis.zadd(queueKey, Date.now(), sessionId); // sorted set, score = timestamp (FIFO)
const position = await redis.zrank(queueKey, sessionId);
return { position: (position ?? 0) + 1 };
}
// Periodically admit batches to checkout
export async function admitFromQueue(productId, batchSize) {
const admitted = await redis.zpopmin(`sale_queue:${productId}`, batchSize);
// Notify each admitted customer they can proceed to checkout
for (let i = 0; i < admitted.length; i += 2) {
await notifyCustomerAdmitted(admitted[i], productId);
}
}
ベストプラクティス
- 在庫はデータベースではなく Redis で確保する — Redis のアトミック操作なら、1秒あたり数千件の同時予約を処理できます。同じ負荷の下で PostgreSQL の行ロックを使うと、タイムアウトやデッドロックが発生します
- 急増時は注文をキューに受け付ける — ピーク時でも、ユーザー向けのチェックアウトは100ms未満で応答する必要があります。決済の確定、データベースへの書き込み、メール送信はバックグラウンドワーカーに委ねます
- すべての外部呼び出しに厳しめのタイムアウトを設定する — 負荷時に Stripe のタイムアウトを30秒にすると、接続が何千件も占有されます。タイムアウトは5秒に設定し、サーキットブレーカーの段階的な発動を使います
- 想定ピークの2~3倍で負荷テストを行う — 想定容量ちょうどでのテストでは余裕がありません。トラフィックの偏りを考慮して、3倍の規模に対応できるようにします
- 待機列の状況を顧客に伝える — 待合室ではリアルタイムの待ち順位を表示します。進捗が見える顧客は、スピナーを見続ける顧客よりもはるかに辛抱強く待ってくれます
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| 在庫を確認しているのに過剰販売が起きる | 確認と減算を行う Redis のアトミックな Lua スクリプトを使います。在庫をアプリケーション層で確認してから、2つの操作に分けて更新することは避けてください |
| オートスケーリングの応答が遅すぎる | イベントの30分前に最小容量まで事前ウォームアップします。スケールアウトのクールダウンは、デフォルトの5分ではなく30秒に設定します |
| 短時間のレイテンシ急増でサーキットブレーカーが開く | volumeThreshold と errorThresholdPercentage を慎重に調整します。フラッシュセールでは、エラー率よりも timeout を主な発動条件として使います |
| 急増時に WooCommerce で過剰販売が起きる | WooCommerce の在庫管理を有効にし、Hold stock を60分に設定します。データベースのロック競合を減らすため、Redis Object Cache を備えたホスティングにアップグレードします |
Related Skills
- @database-optimization-commerce
- @ecommerce-caching
- @monitoring-alerting-commerce
- @load-testing-commerce
- @edge-commerce