O Supabase devolve no máximo 1.000 linhas por consulta, e o limite vale para a API inteira. Se você pede 20.000 com .limit(20000), o pedido não é recusado: volta um pedaço, e qualquer soma ou contagem feita em cima dele fica errada sem que nenhum erro apareça. A saída é somar no banco, ou paginar e comparar com o total real.
O que a documentação diz
Conferi as fontes em 10 de outubro de 2026. A referência do Supabase diz que "por padrão, projetos Supabase devolvem no máximo 1.000 linhas", que isso pode ser mudado nas configurações da API do projeto e que o caminho para ler mais é paginar com range().
Por baixo da API do Supabase está o PostgREST, o programa que transforma tabelas do Postgres em endereços HTTP. A configuração dele se chama db-max-rows e a documentação a descreve como "um limite rígido" ao número de linhas que ele busca de uma view, tabela ou função. O motivo declarado é limitar o tamanho da resposta a pedidos acidentais ou maliciosos. Nessa página, o padrão do PostgREST puro é sem limite. O 1.000 vem do Supabase, e num Supabase que você mesmo hospeda o valor é o que estiver na variável PGRST_DB_MAX_ROWS do contêiner.
Não vi documentado se o corte gera algum aviso. No que medi, não gera.
O que vi em produção
Numa tela de relatório que eu mantinha, a consulta lia os eventos crus dos últimos 7 dias com .limit(20000) e somava no código. O contêiner do PostgREST rodava com PGRST_DB_MAX_ROWS=1000. Eram 5.263 eventos no período, a tela recebeu os 1.000 mais recentes e mostrava 736 ações de um certo tipo em 7 dias. O banco tinha 3.403.
O número parecia plausível porque os 1.000 mais recentes eram quase todos de um tipo só, e o tipo que importava aparecia pouco, mas aparecia. Ninguém comparou o total da tela com uma contagem no banco.
A vizinha do problema veio no mesmo dia. Uma função (RPC) que devolve uma linha por evento também é cortada. A de uma área logada mandava 1.000 de 22.497 eventos, todos de um tipo que depois passei a filtrar. Com o filtro, a lista ficou vazia e o bloco inteiro sumiu da tela, embora houvesse 17.626 registros no banco. Eu tinha consertado as consultas diretas às tabelas e esquecido as funções.
Houve ainda uma terceira forma. Um cálculo de "último acesso de cada conta" lia todas as linhas de entrada ordenadas e ficava com a primeira de cada conta. Com 1.260 entradas, a conta que entrou há mais tempo caía fora das 1.000 primeiras e aparecia como "nunca entrou".
Por que acontece
O .limit() do cliente só consegue pedir menos que o teto, nunca mais. O teto é do servidor. Quem escreve o código vê um número grande no .limit() e entende que está pedindo tudo. A resposta volta com status 200 e uma lista de tamanho plausível, então nada parece errado. Somar um pedaço como se fosse o todo dá um número que parece verdadeiro.
O que fazer
- Some no banco. Em vez de trazer linhas e somar no código, use uma função que devolva o agregado:
create function eventos_por_tipo(desde timestamptz)
returns table (tipo text, total bigint)
language sql stable as $$
select tipo, count(*) from eventos
where criado_em >= desde
group by tipo
$$;
- Se precisar das linhas cruas, peça o total junto com
count: 'exact'e compare com o tamanho do que voltou. A documentação do PostgREST diz que o cabeçalhoContent-Rangeinforma o intervalo devolvido e quePrefer: count=exacttraz o total real, ao custo de uma consulta mais lenta em tabela grande. - Ou pagine com
range(), sempre comorderdefinido, porque a faixa segue a ordenação e sem ela a documentação avisa que o resultado pode variar. - "O último de cada grupo" montado no código sobre um
selectsem filtro tem o mesmo defeito. Faça uma consulta comlimit 1por grupo, ou usedistinct onno banco. - Ao consertar uma tela, varra também as funções que devolvem uma linha por registro, não só as chamadas
.from(). - Para provar que ficou certo, compare o número da tela com uma contagem feita direto no banco, e meça passando pela API com o mesmo papel de acesso do usuário. Medir só no terminal do banco não passa pelo PostgREST e esconde o teto.
Para conferir o teto num Supabase próprio, olhe a variável PGRST_DB_MAX_ROWS do contêiner da API. No Supabase hospedado, a configuração fica nas configurações da API do projeto.
Se você tem um painel cujos totais não batem com o banco, é o tipo de coisa que eu investigo em painéis e dados.
Fontes
- Supabase: por padrão o projeto devolve no máximo 1.000 linhas, ajustável nas configurações da API, e range() para paginar
- PostgREST: db-max-rows é um limite rígido de linhas por view, tabela ou função
- PostgREST: Content-Range informa o intervalo devolvido e Prefer count=exact traz o total
- Supabase: range() usa posições começando em 0 e depende da ordenação