Pular para o conteúdo

Noções de segurança

Supabase "canceling statement due to statement timeout", explicado

Supabase "canceling statement due to statement timeout": uma consulta passou do limite do seu papel, 3 segundos para um visitante. Veja antes a política RLS.

Vlad Tkachenko13 min de leitura
Um cronômetro com o ponteiro além da marca, na frente de uma prateleira longa de linhas em que só as primeiras têm um visto.

Em resumo

  • "canceling statement due to statement timeout" no Supabase significa que o Postgres parou uma consulta porque ela durou mais do que o papel que a enviou permite: 3 segundos para um visitante sem login, 8 para um com login. Nada do que a consulta estava fazendo é mantido.
  • A mesma consulta funciona no SQL Editor porque o editor trabalha com o papel postgres, que não tem limite próprio e pula por completo as suas políticas de Row Level Security. O seu app aplica cada política a cada linha que lê.
  • Conserte a política antes de mexer no limite: envolva as chamadas de função em (select …), crie um índice na coluna que a política compara e nomeie o papel. Um limite maior para visitantes dá a qualquer desconhecido mais tempo com o seu banco.

A página do seu app carrega a lista, o indicador gira por alguns segundos e depois nada aparece. No console do navegador, ou num aviso vermelho que o seu builder colocou, há uma linha:

canceling statement due to statement timeout

Você abre a mesma tabela no Supabase e todas as linhas estão lá. Pergunta ao builder com que fez o app (Lovable, Bolt, v0, Cursor, Claude Code ou Replit) qual consulta aquela página executa, cola no SQL Editor, e ela volta num instante.

O primeiro conselho que você vai achar para "canceling statement due to statement timeout" no Supabase é aumentar o limite. Esse limite está fazendo o trabalho dele, e todo visitante do seu app o divide. Antes de aumentar qualquer coisa, olhe a política de Row Level Security dessa tabela, a regra que o Postgres verifica antes de entregar cada linha. Nas medições do próprio Supabase, é nessa regra que uma consulta que deveria levar 7 milissegundos leva 11 segundos.

O que significa "canceling statement due to statement timeout" no Supabase?

O Postgres, o banco de dados dentro do seu projeto Supabase, parou a consulta porque ela durou mais do que o papel que a enviou permite, e desfez o que a consulta tinha feito.

A frase vem do próprio Postgres, com o código de erro 57014, que o Postgres chama de query_canceled. O Supabase repassa a frase sem mudanças ao seu app, então o erro que o seu código recebe traz code: '57014' e essa frase como mensagem. O Postgres usa o mesmo código para canceling statement due to user request, um cancelamento que alguém pediu, por isso a frase diz mais do que o número.

Pense no seu banco como uma biblioteca, com um balcão e um timer de cozinha. Cada vez que alguém pede livros, a bibliotecária liga o timer antes de ir às estantes, e quanto tempo ele dura depende de quem pede. Quando ele toca, cada livro volta para a estante e o visitante recebe um papel com essa frase. Se o pedido era para mudar alguma coisa, a mudança também é desfeita.

Por que a consulta funciona no SQL Editor e falha no meu app?

Porque o SQL Editor pergunta como outro visitante, um sem timer próprio e sem regras para verificar.

Cada pedido chega ao Postgres com um papel, um nome que decide o que o pedido pode fazer. O guia de timeouts do Supabase lista o limite com que cada papel começa:

Quem perguntaPapelTempo permitido
Um visitante do seu app sem loginanon3 segundos
Um usuário do seu app com loginauthenticated8 segundos
O seu servidor, com a chave secretaservice_rolenenhum próprio, então valem os 8 segundos do papel authenticator
Você, no dashboard e no SQL Editorpostgresnenhum próprio, com um teto padrão de 2 minutos

O papel do editor tem uma segunda vantagem. O guia de Row Level Security do Supabase diz que no Supabase o postgres tem bypassrls, o atributo do Postgres que pula todas as políticas. Então o editor responde sem ler uma regra sequer. O pedido do seu app lê cada política da tabela em cada linha que olha, com um timer que toca aos três segundos para um visitante sem login. O editor cronometrou uma consulta diferente da que o seu app envia.

Na biblioteca, você é da equipe. Entra no acervo sem timer e ninguém pede a sua carteirinha. O visitante tem três segundos, e a bibliotecária verifica as regras em cada livro pelo caminho.

Uma consulta, três papéis. O papel do editor pula a política e não tem linha para cruzar; um visitante sem login passa por cada verificação e é parado aos três segundos.

Encontre a consulta que foi cancelada

Procure nos logs do Postgres do seu projeto as palavras statement timeout, e anote qual papel enviou a consulta.

O guia de timeouts do Supabase traz uma consulta para isso. No dashboard, abra Explorer, escolha Run SQL, defina Logs como origem, escolha um intervalo de tempo que cubra a falha e rode esta versão encurtada:

select timestamp,
       log_attributes['parsed.user_name'] as user_name,
       log_attributes['parsed.query'] as query
from logs
where source = 'postgres_logs'
  and match(event_message, 'statement timeout')
order by timestamp desc
limit 100;

Um user_name igual a authenticator quer dizer que o pedido veio pela Data API, o servidor por onde passa cada chamada supabase.from(…) do seu app. A tabela de papéis do guia lista authenticator como PostgREST, que é esse servidor. A coluna query é o SQL em que o pedido do seu app se transformou, e o nome de tabela que aparece ali é a tabela para olhar.

A página Query Performance do dashboard é o outro lugar onde se costuma olhar, e o guia avisa que ela só mostra consultas lentas que terminaram. Uma consulta cancelada está nos logs.

Se os logs mostram cancelamentos em todas as tabelas ao mesmo tempo, a parte lenta não é uma política só, e distinguir uma queda do Supabase de um problema no seu app é por onde começar.

Meça como o seu app executa

Rode a consulta com o papel do seu app e explain analyze na frente, e o Postgres mostra quanto tempo ela levou e quantas linhas a política recusou.

É o método do guia do Supabase sobre desempenho de Row Level Security, dentro de begin e rollback para a troca de papel acabar junto com o bloco. Copie o id de um usuário real em Authentication → Users no lugar dos zeros:

begin;
set local role authenticated;
set local request.jwt.claims to '{"role":"authenticated", "sub":"00000000-0000-0000-0000-000000000000"}';
explain analyze select * from public.orders;
rollback;

Duas linhas da saída importam. Execution Time é o número para comparar com 3 e 8 segundos. Rows Removed by Filter conta as linhas que o Postgres leu e a política recusou, e o guia do Supabase a chama de sinal a observar: uma política que descarta a maior parte da tabela a cada leitura é uma política cuja coluna de filtro precisa de um índice.

Para medir o que um visitante sem login recebe, escreva set local role anon; e tire a linha dos claims.

O mesmo guia sugere rodar a consulta uma vez com Row Level Security ligado e outra desligado, para ver se o custo está na política. Ele também diz para fazer isso só fora da produção, porque desligar expõe cada linha. Num app no ar, compare a política com a versão reescrita dela.

Como uma política pode custar segundos?

O Postgres verifica a política em cada linha que a consulta lê, e uma chamada de função dentro da política roda de novo para cada uma dessas linhas.

O Supabase publicou medições disso, numa tabela de 100.000 linhas. Quatro delas, o jeito lento e o rápido:

O que a política verificaEscrita comoLevouReescrita comoLevou
Linhas que pertencem ao usuário com loginauth.uid() = user_id179 ms(select auth.uid()) = user_id9 ms
A mesma regra, sem índice na colunaauth.uid() = user_id171 msa mesma, com um índice em user_idmenos de 0,1 ms
Uma checagem de admin que lê outra tabelais_admin()11.000 ms(select is_admin())7 ms
Um papel buscado em outra tabelahas_role() = role178.000 ms(select has_role()) = role12 ms

As duas primeiras são lentas e ainda assim ficam abaixo de três segundos nessa tabela. As duas últimas passam de qualquer limite que um pedido de app tenha. Uma função helper que lê uma segunda tabela, do tipo que se escreve para saber se alguém é admin, é chamada uma vez para cada linha da primeira, então nessa tabela ela leu a segunda 100.000 vezes.

Na biblioteca, esse helper é a bibliotecária indo até o escritório da entrada perguntar se você é da equipe, uma vez para cada livro da estante. Envolvê-lo em (select …) é perguntar uma vez e anotar a resposta no papel.

O Performance Advisor do Supabase pega parte disso. A regra dele chamada Auth RLS Initialization Plan aponta uma política que "re-evaluates current_setting() or auth.() for each row". Um helper seu, como is_admin(), fica fora dessa descrição, então uma página do advisor sem alertas não verificou os seus helpers. Leia você mesmo as políticas da tabela procurando qualquer chamada de função que não esteja envolvida.

Conserte nesta ordem

Envolva as chamadas de função, crie um índice na coluna que a política compara e depois nomeie o papel. Cada passo é um trecho curto de SQL que você pode rodar no editor ou passar para o seu builder.

1. Envolva cada chamada de função em (select …).

alter policy "read own orders" on public.orders
  using ( (select auth.uid()) = user_id );

alter policy mantém o nome e o papel da política e troca a condição. Use o nome da sua política e a coluna da sua tabela, e faça o mesmo com um helper: (select private.is_admin()). O Supabase põe uma condição: isso só funciona para um valor que não muda de uma linha para outra. Por que os parênteses fazem diferença está em o guia para ligar o Row Level Security, e um helper escrito para resolver um erro de recursão infinita é exatamente o tipo que precisa deles.

2. Crie um índice na coluna que a política compara.

create index orders_user_id_idx on public.orders (user_id);

O guia do Supabase acrescenta um porém. Uma coluna só conta como indexada quando vem primeiro no índice, então uma tabela cuja chave primária é (team_id, user_id) não tem índice em user_id. Numa tabela grande que recebe escritas enquanto você trabalha, a documentação do Postgres diz que um create index comum trava a tabela contra escritas até terminar. create index concurrently cria o índice sem essa trava, e precisa rodar como um comando sozinho, fora de uma transação.

3. Nomeie o papel a que a política se destina. Uma política escrita to authenticated nunca roda para um visitante sem login. Nas medições do Supabase, um pedido sem login contra uma política sem to levou 170 ms, e com to authenticated menos de 0,1 ms.

4. Filtre também no pedido. Uma política funciona como um where invisível em cada consulta, e o guia do Supabase recomenda repeti-lo no pedido do seu app, .eq('user_id', userId), porque o Postgres consegue planejar uma consulta mais rápida com ele. Nas medições deles, isso levou uma consulta de 171 ms para 9.

5. Tire o join de uma política de equipe. Uma política que compara cada linha com uma tabela de membros faz essa busca linha por linha:

using ( (select auth.uid()) in (
  select user_id from team_user where team_user.team_id = projects.team_id
) )

O guia do Supabase a reescreve para ler as equipes uma vez e comparar com essa lista:

using ( team_id in (
  select team_id from team_user where user_id = (select auth.uid())
) )

Depois rode de novo o explain analyze de antes e compare Execution Time e Rows Removed by Filter com o que você tinha.

Sem índice, a política lê cada linha para achar as poucas que são suas. Com um índice, o Postgres vai direto a elas.

Devo aumentar o statement timeout?

Só para uma consulta que você mediu, reescreveu e ainda precisa, e aí para essa função, não para um papel inteiro.

Os limites são de propósito. O guia de importação de dados do Supabase diz que eles existem para dividir os recursos de forma justa e impedir que consultas longas afetem o sistema todo. O limite de anon é dividido por todos os visitantes sem login, e qualquer um pode mandar esse pedido com a chave publicável que vai dentro do seu app. Subir de três para trinta segundos dá a cada um desses pedidos trinta segundos do seu banco.

Quando um trabalho realmente leva mais tempo, dê o limite à função que o executa. O guia do Supabase descreve isso para funções recorrentes que precisam de uma exceção no tempo de execução, e funciona para uma função que o seu app chama com supabase.rpc(…):

alter function public.monthly_totals() set statement_timeout to '20s';

Se mesmo assim você mudar um papel, os comandos do guia são alter role authenticated set statement_timeout = '15s'; e depois notify pgrst, 'reload config';, para a Data API pegar o valor novo. O Supabase limita a 60 segundos as consultas das suas bibliotecas cliente, seja qual for o valor que você definir.

Duas correções que calam a mensagem e abrem a tabela

Desligar o Row Level Security na tabela, e mandar o pedido com a chave secreta. As duas deixam a consulta rápida porque as duas pulam a política, e as duas entregam as linhas a pessoas que nunca deviam tê-las.

Desligar. Sem política para rodar, nada é verificado por linha e o timeout some. O Supabase imprime o próprio aviso ao lado do teste que sugere isso: desligar o Row Level Security expõe cada linha da tabela a qualquer papel com a permissão correspondente. O guia dele também diz que, em projetos existentes, uma tabela nova em public começa com select, insert, update e delete concedidos a anon, o papel de um visitante sem login. Sem a política, essas permissões são tudo o que fica entre esse visitante e a tabela.

A chave secreta. O guia do Supabase diz que o papel service_role por trás dela também tem bypassrls, então nenhuma política roda. Essa chave fica num servidor, e uma enviada pelo navegador é uma chave que qualquer um consegue ler no seu site; quais chaves são seguras num frontend explica o par. Ela também não ganha muito tempo, porque o guia dá ao service_role os mesmos 8 segundos do authenticator.

Se alguma tabela sua responde a um pedido sem login nenhum é uma das nove verificações da análise gratuita da Reeve. Ela lê o seu site no ar do jeito que um visitante vê, leva uns 20 segundos e não precisa de conta.

Mantendo a política rápida e a tabela fechada

Uma política que você consertou hoje vai ter uma próxima versão. O seu builder escreve políticas sempre que um recurso precisa de uma, então a próxima pode voltar a chamar uma função por linha, ou abrir a tabela para uma tela carregar.

Reeve Monitor verifica o seu app de fora, a cada hora:

  • as nove verificações a cada hora, inclusive se alguma tabela responde a um pedido sem login
  • se o app está no ar, a cada 60 segundos
  • uma mensagem quando um resultado muda, para que uma tabela que começa a responder a desconhecidos não espere você olhar
  • um relatório mensal do que ele viu

Reeve Care guarda uma cópia do seu banco Supabase de antes da sua mudança:

  • uma cópia criptografada conforme uma agenda, guardada no armazenamento do próprio Reeve na UE, fora da sua conta Supabase
  • cada cópia verificada antes de contar, contando as linhas de cada tabela
  • uma restauração em um clique que salva o que está lá agora antes de substituir
  • também os seus arquivos enviados, assim que você conectar uma credencial de Storage
  • tudo o que o Monitor faz

O que fazer hoje

O que fazer

  • Procure statement timeout nos logs do Postgres e anote a consulta e o papel que a enviou.
  • Meça essa consulta com explain analyze como anon ou authenticated, os papéis que o seu app usa, e leia Execution Time e Rows Removed by Filter.
  • Envolva em (select …) cada chamada de função nas políticas dessa tabela, inclusive os seus próprios helpers que o Performance Advisor não aponta.
  • Crie um índice na coluna que cada política compara, e acrescente to authenticated a cada política feita só para usuários com login.
  • Aumente um limite por último, na única função que precisa, e deixe o Row Level Security ligado.

Comece pela tabela por trás da página que falhou. Se ela ainda não tem políticas próprias, ligar o Row Level Security em cada tabela tem as três formas de que a maioria das tabelas precisa.

Perguntas frequentes

O que significa "canceling statement due to statement timeout" no Supabase?

O Postgres parou a sua consulta porque ela durou mais do que o papel que a enviou permite, e desfez o que ela tinha feito. O Supabase dá 3 segundos a um visitante sem login e 8 a um usuário com login. O erro traz o código 57014, que o Postgres também usa para uma consulta cancelada a pedido, então leia a frase além do número.

Por que a minha consulta funciona no SQL Editor mas dá timeout no meu app?

Porque o SQL Editor trabalha com o papel postgres e o seu app não. O Supabase não dá ao postgres um limite próprio, e o papel tem bypassrls, então o editor nunca executa as suas políticas de Row Level Security. O seu app roda como anon ou authenticated, com limite de 3 ou 8 segundos e cada política verificada em cada linha que a consulta lê.

Row Level Security pode causar um statement timeout?

Pode. O Postgres verifica a política em cada linha que a consulta lê, e uma chamada de função dentro da política roda de novo para cada uma. Em medições que o Supabase publicou para uma tabela de 100.000 linhas, uma política que chamava um helper is_admin() levou 11.000 milissegundos, e a mesma política com a chamada envolvida como (select is_admin()) levou 7.

Como aumento o statement timeout no Supabase?

Para um trabalho lento específico, defina-o na função que o executa com alter function … set statement_timeout. Para um papel inteiro, rode alter role com o novo statement_timeout e depois notify pgrst, 'reload config' para a Data API pegar o valor. O Supabase limita a 60 segundos as consultas das suas bibliotecas cliente. Conserte a política antes, porque o limite de anon é dividido por todos os visitantes sem login.

Devo desligar o RLS para a consulta ficar mais rápida?

Não. A consulta fica mais rápida porque nenhuma política roda, e a tabela passa a ficar aberta a qualquer papel com permissão sobre ela. O guia do Supabase diz que, em projetos existentes, uma tabela nova em public começa com select, insert, update e delete concedidos a anon, o papel de um visitante sem login. Envolva as chamadas de função e crie um índice.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Faça backup do seu banco Supabase antes da próxima mudança

O Reeve guarda uma cópia criptografada em horários programados e a restaura para você se algo quebrar.

Ver backups do Supabase

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.