Construa um Motor de Leilão em Tempo Real com Supabase Realtime
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
- Schema do Banco de Dados e Configuração
- Triggers do PostgreSQL para Lógica de Lances
- Subscrição do Lado do Cliente com JavaScript
- Tratando Condições de Corrida e Validação de Lances
- Rastreamento de Presença para Licitantes Ativos
- Ajuste de Desempenho e Considerações de Produção
- Supabase Realtime vs Alternativas
- Implantando e Escalando Seu Sistema de Leilão
- FAQ
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:
- Licitante envia um lance através do frontend
- Uma chamada RPC ou inserção direta atinge sua tabela
bids - Um trigger do PostgreSQL valida o valor do lance e atualiza
auctions.current_high_bid - Supabase Realtime captura a mudança WAL e a envia para todos os subscribers do canal daquele leilão
- Um segundo trigger dispara um evento Broadcast para notificar o licitante anterior que foi superado
- 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: 20na 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
requestAnimationFramepara 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.