Seu primeiro motor de leilão consultava o banco de dados a cada segundo. Lances chegavam fora de ordem. Alguns desapareciam completamente. Você adicionou um servidor WebSocket separado na segunda versão — melhor, mas agora você está gerenciando duas infraestruturas. A terceira tentativa usou Supabase Realtime, e as condições de corrida pararam. Sem loop de polling. Sem servidor socket independente. Os triggers do PostgreSQL transmitem atualizações de lances no momento em que são confirmados, e cada cliente conectado vê o mesmo estado em menos de 50 milissegundos. Entreguei três sistemas de leilão em dois anos. A versão Supabase é a única que não precisei reconstruir após o lançamento. Aqui está a arquitetura que a fez funcionar — e a função que quase a quebrou.

Supabase Realtime fica no topo do Write-Ahead Log (WAL) do PostgreSQL e usa um servidor baseado em Elixir para enviar mudanças do banco de dados por WebSockets aos clientes conectados. Para um sistema de leilão, isso significa que cada lance que atinge seu banco de dados se propaga instantaneamente para cada licitante observando aquele leilão. Sem polling. Sem infraestrutura pub/sub separada. Seu banco de dados é seu sistema de eventos.

Vamos construir um do zero.

Sumário

Visão Geral da Arquitetura

Antes de escrever qualquer código, vamos entender o que estamos construindo e como as peças se encaixam.

Supabase Realtime oferece três primitivos que mapeiam perfeitamente para requisitos de leilão:

  • Postgres Changes: Subscreva eventos INSERT, UPDATE e DELETE nas suas tabelas de lances e leilões. Quando alguém faz um lance, cada subscritor recebe os dados da nova linha em milissegundos.
  • Broadcast: Envie mensagens efêmeras para participantes do canal. Perfeito para notificações "você foi superado" que não precisam ser persistidas.
  • Presence: Rastreie quem está atualmente observando um leilão. Isso permite mostrar "14 licitantes observando" em sua UI e detectar sessões fantasma.

O fluxo de dados parece assim:

  1. Licitante envia um lance através do frontend
  2. Uma chamada RPC ou inserção direta atinge sua tabela bids
  3. Um trigger do PostgreSQL valida o valor do lance e atualiza auctions.current_high_bid
  4. Supabase Realtime captura a mudança WAL e a envia para todos os subscribers do canal daquele leilão
  5. Um segundo trigger dispara um evento Broadcast para notificar o licitante anterior que foi superado
  6. Cada cliente conectado atualiza sua UI em tempo real

A latência de colocação de lance para atualização de UI em todos os clientes é tipicamente inferior a 100ms. Medi p99 em torno de 80-90ms em produção na tier Pro do Supabase.

Por Que Não Apenas Usar Polling?

Eu sei que alguns de vocês estão pensando "não posso apenas fazer polling a cada 500ms?" Pode. Mas com 200 licitantes simultâneos em um único leilão, são 400 requisições por segundo atingindo seu banco de dados para um leilão. Multiplique isso por 50 leilões ativos e você está em 20.000 consultas por segundo — a maioria das quais não retorna nada novo. WebSockets invertem este modelo: zero consultas quando nada muda, atualizações instantâneas quando algo muda.

Schema do Banco de Dados e Configuração

Aqui está o schema que uso. É intencionalmente simples — você pode estendê-lo, mas a estrutura principal trata da maioria dos tipos de leilão.

-- Tabela de leilões
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()
);

-- Tabela de lances
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)
);

-- Índice para buscas rápidas de lances por leilão
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);

-- Crítico: ative replica identity para Realtime
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

A configuração REPLICA IDENTITY FULL é essencial. Sem ela, Supabase Realtime obtém apenas a chave primária em eventos UPDATE e DELETE — não os dados da linha completa. Para um sistema de leilão, você precisa do payload completo para que os clientes atualizem valores de lances sem fazer uma consulta separada.

Habilitando Replicação Realtime

No Painel do Supabase, vá para Database → Replication e ative a replicação para as duas tabelas auctions e bids. Alternativamente, você pode fazer isso com SQL:

BEGIN;
  -- Remova publicação existente se ela existir
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- Crie publicação com ambas as tabelas
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

Row-Level Security

Não pule isso. RLS é sua camada de validação do lado do servidor.

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

-- Qualquer pessoa pode visualizar leilões ativos
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- Usuários autenticados podem visualizar todos os lances em leilões ativos
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'
    )
  );

-- Apenas usuários autenticados podem fazer lances
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()
    )
  );

Triggers do PostgreSQL para Lógica de Lances

Aqui está onde a verdadeira mágica acontece. O banco de dados impõe toda a lógica de lance do lado do servidor — o cliente não pode trapacear.

Validação de Lance e Trigger de Atualização de Leilão

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- Bloqueie a linha do leilão para prevenir condições de corrida
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- Valide que o leilão está ativo
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- Valide que o leilão não terminou
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- Valide que o valor do lance excede o atual + incremento mínimo
  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;

  -- Previna auto-superação
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- Atualize o leilão com o novo lance mais alto
  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();

Esse bloqueio FOR UPDATE na linha do leilão é crítico. Sem ele, dois lances chegando simultaneamente poderiam ambos ler o mesmo current_high_bid, ambos passar na validação, e ambos serem inseridos. O bloqueio serializa o acesso.

Transmissão de Notificações de Superação

Este trigger dispara após um lance bem-sucedido e envia uma notificação efêmera para o canal do leilão:

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- Encontre quem acaba de ser superado
  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;

  -- Transmita notificação de superação se houve um lance anterior
  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();

Subscrição do Lado do Cliente com JavaScript

Agora vamos conectar o frontend. Vou mostrar isso com padrões vanilla JavaScript/React — a mesma abordagem funciona se você está construindo com Next.js ou qualquer outro framework.

Inicializar o Cliente

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 // Restrinja para tráfego de leilão
      }
    }
  }
);

Esse parâmetro eventsPerSecond importa. Em um leilão movimentado com dezenas de lances por segundo, você não quer re-renderizar 50 vezes por segundo. Vinte atualizações por segundo são mais do que suficientes para uma UI suave.

Subscrever a um Canal de Leilão

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

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

    // Ouça mudanças de status do leilão
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // Ouça notificações de transmissão de superação
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Rastreie licitantes ativos 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') {
        // Rastreie a presença deste usuário
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

React Hook para Subscrições de Leilão

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(() => {
    // Busque estado inicial
    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();

    // Subscreva atualizações em tempo real
    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 };
}

Tratando Condições de Corrida e Validação de Lances

Condições de corrida são a maior fonte de bugs em sistemas de leilão. Aqui está como as trato.

Lado do Servidor: PostgreSQL Faz o Trabalho Pesado

O SELECT ... FOR UPDATE em nossa função trigger é a primeira linha de defesa. Mas há outro padrão que comecei a usar — locks consultivos para leilões de alta contenção:

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
  -- Gere uma chave de lock determinística a partir do UUID do leilão
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- Adquira lock consultivo (bloqueia lances simultâneos no mesmo leilão)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- Agora é seguro inserir (trigger trata da validação)
  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;

Chame isso do cliente usando RPC do Supabase:

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

Lado do Cliente: UI Otimista com Rollback

Mostre o lance imediatamente na UI, mas esteja pronto para revertê-lo se o servidor rejeitá-lo:

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

  // Mostre imediatamente
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // Lance real chegará via Realtime e substituirá o otimista
  } catch (err) {
    // Remova lance otimista em caso de falha
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

Rastreamento de Presença para Licitantes Ativos

Mostrar quantas pessoas estão observando um leilão cria urgência. O rastreamento de presença é muito simples com Supabase:

// Atualize o status do usuário quando começar a fazer lances
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

No lado da exibição, você pode dividir o estado de presença para mostrar quantos estão ativamente fazendo lances vs. apenas observando:

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
  };
}

Ajuste de Desempenho e Considerações de Produção

Restrição e Debounce

Uma guerra de lances pode gerar dezenas de eventos por segundo. Aqui está o que configuro:

  • Lado do servidor: eventsPerSecond: 20 na configuração do cliente Supabase
  • Lado do cliente: Faça debounce no botão de lance em 300ms para prevenir cliques duplos
  • Atualizações de UI: Use requestAnimationFrame para animações de lista de lances

Tempo de Término do Leilão

Não confie no relógio do cliente. Use um trabalho cron do PostgreSQL via pg_cron:

-- Execute a cada 10 segundos para fechar leilões expirados
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();
  $$
);

Extensão Anti-Snipe

A maioria das plataformas de leilão estende o prazo se um lance chegar nos últimos segundos:

-- Adicione ao trigger process_new_bid
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 vs Alternativas

Usei a maioria desses em produção. Aqui está uma comparação honesta:

Recurso Supabase Realtime Pusher Ably Firebase RTDB Socket.io (self-hosted)
Sincronização nativa de BD ✅ PostgreSQL WAL ❌ Serviço separado ❌ Serviço separado ✅ Árvore JSON ❌ Manual
Latência (p99) ~80-100ms ~60ms ~50ms ~100ms ~40ms (depende da infra)
Máx. eventos/sec 200k+ 10k (Pro) 50k 100k Ilimitado (você escala)
Integração de autenticação Built-in (RLS + JWT) Custom Token-based Firebase Auth Custom
Presença ✅ Built-in ✅ Built-in ✅ Built-in ✅ Built-in ✅ Built-in
Free tier 500K MAU, 200 simultâneos 100 conexões 6M msgs/mês 1GB armazenado $0 (custos de hospedagem)
Preço Pro $25/mês $49/mês $29/mês Pay-as-you-go ~$100-500/mês (AWS)
Melhor para Aplicativos real-time centrados em BD Pub/sub simples Alta confiabilidade Apps móveis Controle total

Para um sistema de leilão especificamente, Supabase vence porque seus lances já estão no PostgreSQL. Você não precisa sincronizar entre um banco de dados e um sistema pub/sub separado. O lance atinge o BD, o BD dispara o push WebSocket. Uma única fonte de verdade.

Se você está construindo em uma arquitetura de CMS headless, Supabase se encaixa naturalmente ao lado da entrega de conteúdo sem adicionar outro serviço para gerenciar.

Implantando e Escalando Seu Sistema de Leilão

Para a maioria dos projetos, a tier Pro gerenciada do Supabase a $25/mês trata confortavelmente até 10.000 leilões diários. Aqui está o que observar:

  • Limites de conexão: A tier Pro oferece 500 conexões Realtime simultâneas. Se você precisar de mais, terá que fazer upgrade ou implementar connection pooling no cliente.
  • Tamanho WAL: Lances em alto volume geram tráfego WAL significativo. Monitore seu replication slot para evitar inchaço de disco.
  • Contagem de canais: Cada leilão obtém seu próprio canal. Com milhares de leilões ativos, teste que seu cliente se desinscreve adequadamente de leilões terminados.

Para um frontend construído com Astro ou Next.js, o cliente JS do Supabase funciona identicamente — apenas certifique-se de que está inicializando-o lado do cliente para subscrições Realtime.

Se você está construindo algo que precisa lidar com escala séria — centenas de milhares de licitantes simultâneos — entre em contato conosco. Arquitetamos esses sistemas em escala e podemos ajudá-lo a evitar as armadilhas. Você também pode verificar nossa página de preços para engajamentos baseados em projeto.

FAQ

Quantos licitantes simultâneos o Supabase Realtime pode suportar?

Supabase Realtime pode suportar mais de 200.000 eventos por segundo em servidores distribuídos em sua plataforma gerenciada. A tier Pro a $25/mês suporta até 500 conexões simultâneas por projeto. Para leilões maiores, a tier Enterprise oferece limites customizados, ou você pode fazer self-host do servidor Realtime (é open source) em sua própria infraestrutura.

O Supabase Realtime é rápido o suficiente para um leilão ao vivo?

Sim. Nos meus testes, a latência end-to-end de inserção de lance para notificação do cliente fica em torno de 50-80ms, com p99 inferior a 100ms. Para contexto, um tempo de reação humano é cerca de 200-300ms, então lances aparecem efetivamente instantâneos. O gargalo raramente é Supabase — é geralmente a conexão de rede do cliente.

Como previno condições de corrida quando duas pessoas fazem lance simultaneamente?

Use bloqueio de nível de linha SELECT ... FOR UPDATE do PostgreSQL dentro de uma função trigger, ou use locks consultivos via pg_advisory_xact_lock(). Isso serializa o processamento de lances por leilão para que apenas um lance seja validado por vez. O lance "perdedor" ainda é validado — ele apenas vê o lance atualizado de alto do vencedor e ou tem sucesso (se ainda for maior) ou falha com um erro apropriado.

Posso usar Supabase Realtime com Next.js ou Astro?

Completamente. O cliente @supabase/supabase-js funciona em qualquer ambiente JavaScript. Para Next.js, inicialize o cliente Supabase em um componente cliente (já que Realtime precisa de WebSockets no navegador) e use-o dentro de hooks useEffect. Para Astro, use-o em ilhas interativas do lado do cliente. O código de subscrição é idêntico independentemente da escolha de framework.

O que acontece se a conexão de um usuário cair durante um leilão?

Supabase Realtime tenta automaticamente reconectar. Quando o cliente se reconecta e se inscreve novamente, recebe o estado atual. Para leilões críticos, recomendo também buscar o estado mais recente do leilão via uma consulta padrão na reconexão para garantir que nada foi perdido durante a janela de desconexão. O sistema Presence removerá automaticamente o usuário desconectado após um timeout.

Como lidar com horários de término de leilão com precisão?

Nunca confie em timers do lado do cliente para horários de término de leilão — eles podem ser manipulados. Use a extensão pg_cron do PostgreSQL para verificar e fechar leilões expirados a cada 10 segundos do lado do servidor. Envie o timestamp do servidor aos clientes para que possam exibir uma contagem regressiva, mas a determinação real de término sempre acontece no banco de dados.

O Supabase Realtime é gratuito para projetos pequenos?

A tier gratuita do Supabase inclui Realtime com até 200 conexões simultâneas e 500.000 usuários mensais ativos. Isso é suficiente para um site de leilão de hobby ou um MVP. Se você está executando uma plataforma de leilão de produção com tráfego significativo, a tier Pro a $25/mês com $0,09/GB de saída é onde você desejará começar. É significativamente mais barato do que executar sua própria infraestrutura WebSocket.

Como testo um sistema de leilão em tempo real localmente?

Use a CLI do Supabase (supabase start) para executar uma instância Supabase local com Realtime ativado. Abra várias abas do navegador para simular múltiplos licitantes. Para testes de carga, uso um simples script Node.js que cria 100+ clientes Supabase e os faz fazer lances um contra o outro em um timer. Isso captura condições de corrida e ajuda você a afinar seu parâmetro eventsPerSecond antes de ir para produção.