最初の入札エンジンは毎秒データベースをポーリングしていました。入札が順不同で到着しました。完全に消えてしまったものもあります。2番目のバージョンでは別々のWebSocketサーバーを追加しました—より良かったのですが、今は2つのインフラストラクチャを管理しています。3番目の試みはSupabase Realtimeを使用し、レース条件は止まりました。ポーリングループはありません。スタンドアロンのソケットサーバーもありません。PostgreSQLトリガーがコミットされた瞬間に入札更新をブロードキャストし、接続されたすべてのクライアントが50ミリ秒以内に同じ状態を見ます。2年間で3つのオークションシステムを出荷しました。Supabaseバージョンは、起動後に再構築する必要がなかった唯一のものです。ここに動作させたアーキテクチャがあります—そしてそれをほぼ壊してしまった1つの関数があります。

Supabase RealtimeはPostgreSQLのWrite-Ahead Log(WAL)の上に位置し、Elixirベースのサーバーを使用してデータベース変更をWebSocket経由で接続されたクライアントにプッシュします。オークションシステムの場合、データベースに当たったすべての入札がそのオークションを見ているすべての入札者に即座に伝播することを意味します。ポーリングなし。別のパブ/サブインフラなし。あなたのデータベースあなたのイベントシステムです。

ゼロから構築してみましょう。

目次

アーキテクチャの概要

コードを書く前に、何を構築しているのか、ピースがどのように組み合わさるのかを理解しましょう。

Supabase Realtimeは、オークション要件に完全にマップされた3つのプリミティブを提供します:

  • Postgres Changes: bidsテーブルとauctionsテーブルの INSERT、UPDATE、および DELETE イベントをサブスクライブします。誰かが入札を入れると、すべてのサブスクライバーは数ミリ秒以内に新しい行データを取得します。
  • Broadcast: チャネル参加者に一時的なメッセージを送信します。「入札で上回られました」という通知に最適で、永続化する必要はありません。
  • Presence: 現在オークションを見ている人を追跡します。これにより、UIに「14の入札者が見ています」を表示し、ゴーストセッションを検出できます。

データフローは次のようになります:

  1. 入札者がフロントエンドを通じて入札を提出します
  2. RPC呼び出しまたは直接挿入がbidsテーブルにヒットします
  3. PostgreSQLトリガーが入札額を検証し、auctions.current_high_bidを更新します
  4. Supabase Realtimeが WAL の変更をピックアップし、そのオークションのチャネルのすべてのサブスクライバーにプッシュします
  5. 2番目のトリガーがブロードキャストイベントを発火させて、前の高い入札者に入札で上回られたことを通知します
  6. 接続されたすべてのクライアントがリアルタイムで UI を更新します

入札配置から全クライアントの UI 更新までのレイテンシーは通常 100ms 未満です。Supabaseの Pro ティアでプロダクション環境で p99 を約 80-90ms で計測しています。

なぜポーリングを使わないのですか?

「500ms ごとにポーリングできませんか?」と思っている人もいることを知っています。できます。しかし、1つのオークションで 200 の同時入札者では、1回のオークションでデータベースに 400 リクエスト/秒が当たります。それを 50 個のアクティブなオークションで乗算すると、1秒あたり 20,000 個のクエリになります—ほとんどが新しい情報を返しません。WebSocket はこのモデルを反転させます:何も変わらないときはゼロクエリ、何かが起こるときは即座の更新。

データベーススキーマとセットアップ

ここは私が使うスキーマです。意図的にシンプルです—拡張できますが、コア構造はほとんどのオークションタイプを処理します。

-- Auctions table
CREATE TABLE auctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_name TEXT NOT NULL,
  description TEXT,
  starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
  current_high_bid DECIMAL(12,2) DEFAULT 0,
  highest_bidder_id UUID REFERENCES auth.users(id),
  min_increment DECIMAL(12,2) DEFAULT 1.00,
  status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
  starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Bids table
CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  amount DECIMAL(12,2) NOT NULL,
  placed_at TIMESTAMPTZ DEFAULT NOW(),
  CONSTRAINT positive_amount CHECK (amount > 0)
);

-- Index for fast bid lookups per auction
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);

-- Critical: enable replica identity for Realtime
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

REPLICA IDENTITY FULL設定は必須です。これなしでは、Supabase Realtimeは UPDATE および DELETE イベント時にプライマリキーのみを取得します—完全な行データはありません。オークションシステムでは、クライアントが別のクエリを作成することなく入札額を更新できるように、完全なペイロードが必要です。

Realtime レプリケーションの有効化

Supabase ダッシュボードで、Database → Replication に移動して、auctionsテーブルとbidsテーブル両方のレプリケーションを切り替えます。または、SQL でこれを行うことができます:

BEGIN;
  -- Remove existing publication if it exists
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- Create publication with both tables
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

行レベルセキュリティ

これをスキップしないでください。RLS はサーバー側検証層です。

ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;

-- Anyone can view active auctions
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- Authenticated users can view all bids on active auctions
CREATE POLICY "View bids on active auctions" ON bids
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = bids.auction_id
      AND auctions.status = 'active'
    )
  );

-- Only authenticated users can place bids
CREATE POLICY "Place bids" ON bids
  FOR INSERT WITH CHECK (
    auth.uid() = user_id
    AND EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = auction_id
      AND auctions.status = 'active'
      AND auctions.ends_at > NOW()
    )
  );

入札ロジック用の PostgreSQL トリガー

これが本当の魔法が起こる場所です。データベースはすべての入札ロジックをサーバー側で実装します—クライアントは不正行為できません。

入札検証とオークション更新トリガー

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- Lock the auction row to prevent race conditions
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- Validate auction is active
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- Validate auction hasn't ended
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- Validate bid amount exceeds current high + minimum increment
  IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
    RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
      v_auction.min_increment, v_auction.current_high_bid;
  END IF;

  -- Prevent self-outbidding
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- Update auction with new high bid
  UPDATE auctions
  SET
    current_high_bid = NEW.amount,
    highest_bidder_id = NEW.user_id,
    updated_at = NOW()
  WHERE id = NEW.auction_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER validate_and_process_bid
  BEFORE INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION process_new_bid();

そのオークション行のFOR UPDATEロックは重要です。これなしでは、同時に到着した2つの入札が同じcurrent_high_bidを読み、両方が検証に合格し、両方が挿入される可能性があります。ロックはアクセスをシリアル化します。

入札外通知のブロードキャスト

このトリガーは成功した入札の後に発火し、オークションチャネルへの一時的な通知を送信します:

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- Find who just got outbid
  SELECT user_id INTO v_previous_bidder
  FROM bids
  WHERE auction_id = NEW.auction_id
    AND id != NEW.id
  ORDER BY amount DESC
  LIMIT 1;

  -- Broadcast outbid notification if there was a previous bidder
  IF v_previous_bidder IS NOT NULL THEN
    PERFORM realtime.send(
      jsonb_build_object(
        'auction_id', NEW.auction_id,
        'new_high', NEW.amount,
        'outbid_user', v_previous_bidder,
        'new_leader', NEW.user_id
      ),
      'outbid',
      'auction:' || NEW.auction_id::text,
      true
    );
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER after_bid_notify
  AFTER INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION notify_outbid();

JavaScript によるクライアント側サブスクリプション

ここでフロントエンドを配線しましょう。これはバニラ JavaScript/React パターンで示します—同じアプローチが Next.js または他のフレームワークで構築している場合に機能します。

クライアントの初期化

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    realtime: {
      params: {
        eventsPerSecond: 20 // Throttle for auction traffic
      }
    }
  }
);

そのeventsPerSecondパラメータは重要です。1秒あたり数十回の入札がある最新のオークションでは、1秒に 50 回 re-render したくはありません。1秒あたり 20 の更新は滑らかな UI には十分以上です。

オークションチャネルのサブスクリプション

function subscribeToAuction(auctionId, callbacks) {
  const channel = supabase.channel(`auction:${auctionId}`);

  channel
    // Listen for new bids via Postgres Changes
    .on('postgres_changes', {
      event: 'INSERT',
      schema: 'public',
      table: 'bids',
      filter: `auction_id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onNewBid(payload.new);
    })

    // Listen for auction status changes
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // Listen for outbid broadcast notifications
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Track active bidders via Presence
    .on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState();
      const bidderCount = Object.keys(state).length;
      callbacks.onPresenceUpdate(bidderCount, state);
    })

    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        // Track this user's presence
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

React フックによるオークションサブスクリプション

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

function useAuction(auctionId) {
  const [auction, setAuction] = useState(null);
  const [bids, setBids] = useState([]);
  const [bidderCount, setBidderCount] = useState(0);
  const [isOutbid, setIsOutbid] = useState(false);

  useEffect(() => {
    // Fetch initial state
    async function loadAuction() {
      const { data: auctionData } = await supabase
        .from('auctions')
        .select('*')
        .eq('id', auctionId)
        .single();
      setAuction(auctionData);

      const { data: bidData } = await supabase
        .from('bids')
        .select('*')
        .eq('auction_id', auctionId)
        .order('amount', { ascending: false })
        .limit(20);
      setBids(bidData || []);
    }
    loadAuction();

    // Subscribe to real-time updates
    const channel = subscribeToAuction(auctionId, {
      onNewBid: (bid) => {
        setBids(prev => [bid, ...prev].slice(0, 20));
        setIsOutbid(false);
      },
      onAuctionUpdate: (updated) => setAuction(updated),
      onOutbid: (payload) => {
        const currentUser = supabase.auth.getUser()?.data?.user;
        if (payload.outbid_user === currentUser?.id) {
          setIsOutbid(true);
        }
      },
      onPresenceUpdate: (count) => setBidderCount(count)
    });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [auctionId]);

  const placeBid = useCallback(async (amount) => {
    const user = (await supabase.auth.getUser()).data.user;
    const { data, error } = await supabase
      .from('bids')
      .insert({
        auction_id: auctionId,
        amount: parseFloat(amount),
        user_id: user.id
      })
      .select()
      .single();

    if (error) throw new Error(error.message);
    return data;
  }, [auctionId]);

  return { auction, bids, bidderCount, isOutbid, placeBid };
}

レース条件と入札検証の処理

レース条件はオークションシステムのバグの最大の原因です。これらを処理する方法を紹介します。

サーバー側:PostgreSQL が重い工事をします

私たちのトリガー関数のSELECT ... FOR UPDATEは最初の防御線です。しかし、私が使い始めた別のパターンがあります—高競合オークション用の協議ロック:

CREATE OR REPLACE FUNCTION place_bid_safe(
  p_auction_id UUID,
  p_user_id UUID,
  p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
  v_lock_key BIGINT;
  v_bid_id UUID;
BEGIN
  -- Generate a deterministic lock key from auction UUID
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- Acquire advisory lock (blocks concurrent bids on same auction)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- Now safe to insert (trigger handles validation)
  INSERT INTO bids (auction_id, user_id, amount)
  VALUES (p_auction_id, p_user_id, p_amount)
  RETURNING id INTO v_bid_id;

  RETURN QUERY
  SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Supabase の RPC を使用してクライアントから呼び出します:

const { data, error } = await supabase.rpc('place_bid_safe', {
  p_auction_id: auctionId,
  p_user_id: user.id,
  p_amount: bidAmount
});

クライアント側:ロールバック付きの最適化 UI

すぐに UI に入札を表示しますが、サーバーがそれを拒否した場合はそれをロールバックする準備ができていてください:

async function handleBidSubmit(amount) {
  const optimisticBid = {
    id: crypto.randomUUID(),
    amount,
    user_id: user.id,
    placed_at: new Date().toISOString(),
    _optimistic: true
  };

  // Show immediately
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // Real bid will arrive via Realtime and replace optimistic one
  } catch (err) {
    // Remove optimistic bid on failure
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

アクティブな入札者のプレゼンストラッキング

オークションを見ている人数を表示することで緊迫感が生まれます。Supabase でのプレゼンストラッキングは非常にシンプルです:

// Update user status when they start bidding
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

表示側では、プレゼンス状態を細分化して、実際に入札している人数と見ているだけの人数を表示できます:

function parseBidderStats(presenceState) {
  const users = Object.values(presenceState).flat();
  return {
    total: users.length,
    bidding: users.filter(u => u.status === 'bidding').length,
    watching: users.filter(u => u.status === 'watching').length
  };
}

パフォーマンスチューニングとプロダクション考慮事項

スロットリングとデバウンス

入札戦争は1秒に数十のイベントを生成することができます。ここは私が設定するものです:

  • サーバー側:Supabase クライアント設定でeventsPerSecond: 20
  • クライアント側:二重クリックを防ぐために入札ボタンを 300ms でデバウンス
  • UI 更新:入札リストアニメーションにrequestAnimationFrameを使用

オークション終了のタイミング

クライアントクロックに依存しないでください—操作される可能性があります。pg_cron経由の PostgreSQL cron ジョブを使用して、10 秒ごとに期限切れのオークションをチェックして閉じてください:

-- Run every 10 seconds to close expired auctions
SELECT cron.schedule(
  'close-expired-auctions',
  '*/10 * * * * *',
  $$
  UPDATE auctions
  SET status = CASE
    WHEN highest_bidder_id IS NOT NULL THEN 'sold'
    ELSE 'ended'
  END
  WHERE status = 'active'
  AND ends_at <= NOW();
  $$
);

アンチスナイプ拡張

ほとんどのオークションプラットフォームは、最後の数秒間に入札が入ってくると締切を延長します:

-- Add to the process_new_bid trigger
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
  UPDATE auctions
  SET ends_at = ends_at + INTERVAL '30 seconds'
  WHERE id = NEW.auction_id;
END IF;

Supabase Realtime と代替案

プロダクション環境でこれらのほとんどを使用しました。ここに誠実な比較があります:

機能 Supabase Realtime Pusher Ably Firebase RTDB Socket.io(自己ホスト)
ネイティブ DB 同期 ✅ PostgreSQL WAL ❌ 独立したサービス ❌ 独立したサービス ✅ JSON ツリー ❌ 手動
レイテンシー (p99) ~80-100ms ~60ms ~50ms ~100ms ~40ms(インフラに依存)
最大イベント/秒 200k+ 10k (Pro) 50k 100k 無制限(スケーリング)
認証統合 ビルトイン(RLS + JWT) カスタム トークンベース Firebase Auth カスタム
プレゼンス ✅ ビルトイン ✅ ビルトイン ✅ ビルトイン ✅ ビルトイン ✅ ビルトイン
無料ティア 500K MAU、200 同時実行 100 接続 600 万メッセージ/月 1GB 保存 $0(ホスティングコスト)
Pro 価格 $25/月 $49/月 $29/月 従量課金 ~$100-500/月(AWS)
ベストな用途 DB中心のリアルタイムアプリ シンプルなパブ/サブ 高信頼性 モバイルアプリ 完全制御

オークションシステム特有では、Supabase が勝ります。入札は既に PostgreSQL にあるためです。データベースと別のパブ/サブシステム間の同期は必要ありません。入札がDBに当たり、DB がWebSocket プッシュをトリガーします。唯一の真実のソース。

ヘッドレス CMS アーキテクチャ上に構築している場合、Supabase は管理する別のサービスを追加することなく、コンテンツ配信と自然にフィットします。

オークションシステムのデプロイとスケーリング

ほとんどのプロジェクトでは、Supabase の管理 Pro ティアが月 $25 で1日最大 10,000 オークションを快適に処理します。ここで見守るべきは:

  • 接続制限:Pro ティアは最大 500 の同時 Realtime 接続を提供します。もっと必要な場合は、アップグレードするか、クライアント側に接続プーリングを実装する必要があります。
  • WAL サイズ:高容量の入札は著しい WAL トラフィックを生成します。レプリケーションスロットを監視して、ディスク膨張を回避してください。
  • チャネル数:各オークションが独自のチャネルを取得します。数千のアクティブなオークションでは、クライアントが終了したオークションから正しくアンサブスクライブされていることをテストしてください。

AstroまたはNext.jsで構築されたフロントエンドの場合、Supabase JS クライアントは同じように機能します—Realtime サブスクリプション用にクライアント側で初期化していることを確認してください。

シリアスなスケールを処理する必要があるものを構築している場合—数十万の同時入札者—当社に連絡してください。私たちはこれらのシステムを大規模で構築し、落とし穴を回避するのに役立てることができます。また、当社の価格ページでプロジェクトベースのエンゲージメントを確認することもできます。

FAQ

Supabase Realtime はいくつの同時入札者を処理できますか?

Supabase Realtime は、マネージド プラットフォームの分散サーバー全体で 1 秒あたり 200,000 を超えるイベントを処理できます。月 $25 の Pro ティアは、プロジェクトあたり最大 500 の同時接続をサポートします。より大規模なオークションでは、Enterprise ティアはカスタム制限を提供するか、独自のインフラストラクチャで Realtime サーバー(オープンソース)をセルフホストできます。

Supabase Realtime はライブオークションに十分に速いですか?

はい。私のテストでは、入札挿入からクライアント通知までのエンドツーエンドレイテンシーは平均 50~80ms で、p99 は 100ms 未満です。参考のため、人間の反応時間は約 200~300ms であるため、入札は事実上インスタントに表示されます。ボトルネックはめったに Supabase ではありません—通常はクライアントのネットワーク接続です。

2 人が同時に入札するときにレース条件をどのように防ぎますか?

トリガー関数内で PostgreSQL のSELECT ... FOR UPDATE行レベルロックを使用するか、pg_advisory_xact_lock()経由の協議ロックを使用してください。これはオークション内の入札処理をシリアル化するため、一度に 1 つの入札のみが検証されます。「負け」入札は検証されます—ウィナーからの更新された高い入札を見ます。成功するか(それでも高い場合)、適切なエラーで失敗します。

Next.js または Astro で Supabase Realtime を使用できますか?

絶対に。@supabase/supabase-jsクライアントは任意の JavaScript 環境で動作します。Next.js の場合、クライアント コンポーネントで Supabase クライアントを初期化(Realtime は ブラウザ WebSocket を必要とするため)し、useEffectフック内で使用します。Astro の場合、クライアント側のインタラクティブアイランド内で使用します。フレームワーク選択に関係なく、サブスクリプション コードは同じです。

ユーザーの接続がオークションの途中で切断された場合はどうなりますか?

Supabase Realtime は自動的に再接続を試みます。クライアントが再接続してサブスクライブし直すと、現在の状態を受け取ります。重要なオークションでは、切断ウィンドウ中に何も逃されないことを確認するために、再接続時に標準クエリを通じて最新のオークション状態をフェッチすることも推奨します。Presence システムはタイムアウト後に自動的に切断されたユーザーを削除します。

オークション終了時間を正確に処理するにはどうすればよいですか?

オークション終了時間のためにクライアント側のタイマーに依存しないでください—操作される可能性があります。pg_cron拡張を使用して、10 秒ごとにサーバー側で期限切れのオークションをチェックして閉じる PostgreSQL cron ジョブを実行してください。カウントダウンを表示できるようにサーバータイムスタンプをクライアントに送信しますが、実際の終了判定は常にデータベースで行われます。

Supabase Realtime は小さなプロジェクト向けに無料ですか?

Supabase の無料ティアには Realtime が含まれており、最大 200 の同時接続と 500,000 の月間アクティブユーザーがあります。これは、ホビーオークションサイトまたは MVP には十分です。実質的なトラフィックを使用してプロダクションオークションプラットフォームを実行している場合、月 $25 の Pro ティアと $0.09/GB エグレスがあなたが始めたいところです。独自の WebSocket インフラストラクチャを実行するよりはるかに安価です。

リアルタイム入札システムをローカルでテストするにはどうすればよいですか?

Supabase CLI(supabase start)を使用して、Realtime が有効化されたローカル Supabase インスタンスを実行します。複数のブラウザタブを開いて複数の入札者をシミュレートします。ロード テスト用に、100 以上の Supabase クライアントを作成し、タイマー上のオークションに互いに入札させる単純な Node.js スクリプトを使用します。これはレース条件をキャッチし、プロダクションに進む前にeventsPerSecondパラメータをチューニングするのに役立ちます。