Por que o Supabase devolve só 1.000 linhas e não avisa? (max rows do PostgREST)

O Supabase corta toda resposta da API em 1.000 linhas por padrão, sem erro. Como o limite max rows engana uma soma na tela e como somar no banco.

Pedro Henrique Quadroatualizado em 4 min de leitura

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

  1. 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
$$;
  1. 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çalho Content-Range informa o intervalo devolvido e que Prefer: count=exact traz o total real, ao custo de uma consulta mais lenta em tabela grande.
  2. Ou pagine com range(), sempre com order definido, porque a faixa segue a ordenação e sem ela a documentação avisa que o resultado pode variar.
  3. "O último de cada grupo" montado no código sobre um select sem filtro tem o mesmo defeito. Faça uma consulta com limit 1 por grupo, ou use distinct on no banco.
  4. Ao consertar uma tela, varra também as funções que devolvem uma linha por registro, não só as chamadas .from().
  5. 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

  1. Supabase: por padrão o projeto devolve no máximo 1.000 linhas, ajustável nas configurações da API, e range() para paginar
  2. PostgREST: db-max-rows é um limite rígido de linhas por view, tabela ou função
  3. PostgREST: Content-Range informa o intervalo devolvido e Prefer count=exact traz o total
  4. Supabase: range() usa posições começando em 0 e depende da ordenação
Pedro Henrique QuadroEngenheiro de software e IA. Constrói aplicativos, sistemas, painéis e agentes de IA em produção, e tem código aceito no Supabase, no Kestra e no QuestDB.

Continue lendo