Fundamentos de seguridad
Supabase "canceling statement due to statement timeout", explicado
Supabase "canceling statement due to statement timeout": una consulta superó el límite de su rol, 3 segundos para un visitante. Revisa antes la política RLS.

En resumen
- "canceling statement due to statement timeout" en Supabase significa que Postgres detuvo una consulta porque duró más de lo que permite el rol que la envió: 3 segundos para un visitante sin sesión, 8 para uno con sesión. No se conserva nada de lo que la consulta estaba haciendo.
- La misma consulta funciona en el SQL Editor porque el editor trabaja con el rol postgres, que no tiene límite propio y se salta por completo tus políticas de Row Level Security. Tu app aplica cada política a cada fila que lee.
- Arregla la política antes de tocar el límite: envuelve las llamadas a funciones en (select …), indexa la columna que compara la política y nombra el rol. Un límite más alto para visitantes da a cualquier desconocido más tiempo con tu base de datos.
La página de tu app carga su lista, el indicador gira unos segundos y luego no aparece nada. En la consola del navegador, o en un aviso rojo que añadió tu builder, hay una línea:
canceling statement due to statement timeout
Abres la misma tabla en Supabase y están todas las filas. Le preguntas al builder con el que hiciste la app (Lovable, Bolt, v0, Cursor, Claude Code o Replit) qué consulta ejecuta esa página, la pegas en el SQL Editor y vuelve en un momento.
El primer consejo que encontrarás para "canceling statement due to statement timeout" en Supabase es subir el límite. Ese límite está haciendo su trabajo, y lo comparte cada visitante de tu app. Antes de subir nada, mira la política de Row Level Security de esa tabla, la regla que Postgres comprueba antes de entregar cada fila. En las mediciones de la propia Supabase, esa regla es donde una consulta que debería tardar 7 milisegundos pasa 11 segundos.
¿Qué significa "canceling statement due to statement timeout" en Supabase?
Postgres, la base de datos que hay dentro de tu proyecto de Supabase, detuvo la consulta porque duró más de lo que permite el rol que la envió, y deshizo lo que la consulta había hecho.
La frase viene del propio Postgres, con el código de error 57014, que Postgres
llama query_canceled. Supabase se la pasa a tu app sin cambios, así que el
error que recibe tu código lleva code: '57014' y esa frase como mensaje.
Postgres usa el mismo código para canceling statement due to user request, una
cancelación que alguien pidió, por eso la frase te dice más que el número.
Imagina tu base de datos como una biblioteca con un mostrador y un temporizador de cocina. Cada vez que alguien pide libros, la bibliotecaria pone el temporizador antes de ir a las estanterías, y cuánto dura depende de quién pregunta. Cuando suena, cada libro vuelve a su sitio y el visitante recibe una nota con esta frase. Si la petición era para cambiar algo, el cambio también se deshace.
¿Por qué la consulta funciona en el SQL Editor y falla en mi app?
Porque el SQL Editor pregunta como otro visitante, uno sin temporizador propio y sin reglas que comprobar.
Cada petición llega a Postgres con un rol, un nombre que decide lo que la petición puede hacer. La guía de timeouts de Supabase da el límite con el que empieza cada rol:
| Quién pregunta | Rol | Tiempo permitido |
|---|---|---|
| Un visitante de tu app sin sesión | anon | 3 segundos |
| Un usuario de tu app con sesión | authenticated | 8 segundos |
| Tu servidor, con la clave secreta | service_role | ninguno propio, así que rigen los 8 segundos del rol authenticator |
| Tú, en el dashboard y en el SQL Editor | postgres | ninguno propio, con un tope por defecto de 2 minutos |
El rol del editor tiene una segunda ventaja. La guía de Row Level Security de
Supabase dice que en Supabase postgres tiene bypassrls, el atributo de
Postgres que se salta todas las políticas. Así que el editor responde sin leer
ni una regla. La petición de tu app lee cada política de la tabla en cada fila
que mira, con un temporizador que suena a los tres segundos para un visitante
sin sesión. El editor midió una consulta distinta de la que envía tu app.
En la biblioteca tú eres del personal. Entras al depósito sin temporizador y nadie te pide el carnet. El visitante tiene tres segundos, y la bibliotecaria comprueba las reglas en cada libro por el camino.
Encuentra la consulta que se canceló
Busca en los logs de Postgres de tu proyecto las palabras statement timeout y
anota qué rol envió la consulta.
La guía de timeouts de Supabase da una consulta para esto. En el dashboard, abre Explorer, elige Run SQL, pon Logs como origen, elige un rango de tiempo que cubra el fallo y ejecuta esta versión recortada:
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;
Un user_name de authenticator significa que la petición llegó por la Data
API, el servidor por el que pasa cada llamada supabase.from(…) de tu app. La
tabla de roles de la guía da authenticator como PostgREST, que es ese
servidor. La columna query es el SQL en que se convirtió la petición de tu
app, y el nombre de tabla que aparece ahí es la tabla que hay que mirar.
La página Query Performance del dashboard es el otro sitio donde se suele mirar, y la guía advierte que solo muestra consultas lentas que terminaron. Una consulta cancelada está en los logs.
Si los logs muestran cancelaciones en todas las tablas a la vez, la parte lenta no es una sola política, y distinguir una caída de Supabase de un problema en tu app es por donde empezar.
Mídela como la ejecuta tu app
Ejecuta la consulta con el rol de tu app y explain analyze delante, y Postgres
imprime cuánto tardó y cuántas filas rechazó la política.
Es el método de la guía de Supabase sobre el rendimiento de Row Level Security,
dentro de begin y rollback para que el cambio de rol termine con el bloque.
Copia el id de un usuario real desde Authentication → Users en lugar de los
ceros:
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;
Dos líneas de la salida importan. Execution Time es el número que comparas con
3 y 8 segundos. Rows Removed by Filter cuenta las filas que Postgres leyó y la
política rechazó, y la guía de Supabase la llama la señal a vigilar: una
política que descarta la mayor parte de la tabla en cada lectura es una política
cuya columna de filtro necesita un índice.
Para medir lo que recibe un visitante sin sesión, escribe set local role anon;
y quita la línea de los claims.
La misma guía sugiere ejecutar la consulta una vez con Row Level Security activada y otra desactivada, para ver si el coste está en la política. También dice que eso se haga solo fuera de producción, porque desactivarla expone cada fila. En una app en producción, mide la política contra su versión reescrita.
¿Cómo puede una política costar segundos?
Postgres comprueba la política en cada fila que lee la consulta, y una llamada a función dentro de la política se vuelve a ejecutar por cada una de esas filas.
Supabase publicó mediciones de esto, sobre una tabla de 100.000 filas. Cuatro de ellas, la forma lenta y la rápida:
| Qué comprueba la política | Escrita como | Tardó | Reescrita como | Tardó |
|---|---|---|---|---|
| Filas que pertenecen al usuario con sesión | auth.uid() = user_id | 179 ms | (select auth.uid()) = user_id | 9 ms |
| La misma regla, sin índice en la columna | auth.uid() = user_id | 171 ms | la misma, con un índice en user_id | menos de 0,1 ms |
| Una comprobación de admin que lee otra tabla | is_admin() | 11.000 ms | (select is_admin()) | 7 ms |
| Un rol buscado en otra tabla | has_role() = role | 178.000 ms | (select has_role()) = role | 12 ms |
Las dos primeras son lentas y aun así quedan por debajo de tres segundos en esa tabla. Las dos últimas superan cualquier límite que tenga una petición de una app. Una función helper que lee una segunda tabla, del tipo que se escribe para saber si alguien es admin, se llama una vez por cada fila de la primera, así que en esa tabla leyó la segunda 100.000 veces.
En la biblioteca, ese helper es la bibliotecaria yendo a la oficina de la
entrada a preguntar si eres del personal, una vez por cada libro de la
estantería. Envolverlo en (select …) es preguntar una vez y apuntar la
respuesta en la nota.
El Performance Advisor de Supabase detecta una parte de esto. Su regla llamada
Auth RLS Initialization Plan señala una política que "re-evaluates
current_setting() or auth.is_admin(), queda fuera de esa descripción, así que una página del advisor
sin avisos no ha revisado tus helpers. Lee tú mismo las políticas de la tabla
buscando cualquier llamada a función que no esté envuelta.
Arréglalo en este orden
Envuelve las llamadas a funciones, indexa la columna que compara la política y luego nombra el rol. Cada paso es un trozo corto de SQL que puedes ejecutar en el editor o pasarle a tu builder.
1. Envuelve cada llamada a función en (select …).
alter policy "read own orders" on public.orders
using ( (select auth.uid()) = user_id );
alter policy conserva el nombre y el rol de la política y sustituye la
condición. Usa el nombre de tu política y la columna de tu tabla, y haz lo mismo
con un helper: (select private.is_admin()). Supabase pone una condición: solo
funciona con un valor que no cambia de una fila a otra. Por qué los paréntesis
marcan la diferencia está en
la guía para activar Row Level Security,
y un helper escrito para
resolver un error de recursión infinita
es justo del tipo que los necesita.
2. Indexa la columna que compara la política.
create index orders_user_id_idx on public.orders (user_id);
La guía de Supabase añade una pega. Una columna solo cuenta como indexada cuando
va primera en el índice, así que una tabla cuya clave primaria es
(team_id, user_id) no tiene índice en user_id. En una tabla grande en la que
se escribe mientras trabajas, la documentación de Postgres dice que un
create index normal bloquea las escrituras en la tabla hasta que termina.
create index concurrently lo construye sin ese bloqueo, y tiene que ejecutarse
como una sentencia aparte, fuera de una transacción.
3. Nombra el rol al que va dirigida la política. Una política escrita
to authenticated nunca se ejecuta para un visitante sin sesión. En las
mediciones de Supabase, una petición sin sesión contra una política sin to
tardó 170 ms, y con to authenticated menos de 0,1 ms.
4. Filtra también en la petición. Una política funciona como un where
invisible en cada consulta, y la guía de Supabase recomienda repetirlo en la
petición de tu app, .eq('user_id', userId), porque Postgres puede planificar
una consulta más rápida con él. En sus mediciones eso llevó una consulta de 171
ms a 9.
5. Saca el join de una política de equipos. Una política que compara cada fila con una tabla de miembros hace esa búsqueda por fila:
using ( (select auth.uid()) in (
select user_id from team_user where team_user.team_id = projects.team_id
) )
La guía de Supabase la reescribe para que lea los equipos una vez y compare con esa lista:
using ( team_id in (
select team_id from team_user where user_id = (select auth.uid())
) )
Después vuelve a ejecutar el explain analyze de antes y compara
Execution Time y Rows Removed by Filter con lo que tenías.
¿Debería aumentar el statement timeout?
Solo para una consulta que has medido, reescrito y sigues necesitando, y entonces para esa función concreta, no para un rol entero.
Los límites son deliberados. La guía de importación de datos de Supabase dice
que los aplica para repartir los recursos de forma justa y evitar que las
consultas largas afecten al sistema entero. El límite de anon lo comparten
todos los visitantes sin sesión, y cualquiera puede enviar esa petición con la
clave publicable que va dentro de tu app. Subirlo de tres segundos a treinta da
a cada una de esas peticiones treinta segundos de tu base de datos.
Cuando un trabajo de verdad tarda más, dale el límite a la función que lo
ejecuta. La guía de Supabase describe esto para funciones recurrentes que
necesitan una excepción en su tiempo de ejecución, y funciona para una función
que tu app llama con supabase.rpc(…):
alter function public.monthly_totals() set statement_timeout to '20s';
Si cambias un rol de todas formas, los comandos de la guía son
alter role authenticated set statement_timeout = '15s'; y después
notify pgrst, 'reload config';, para que la Data API recoja el valor nuevo.
Supabase limita a 60 segundos las consultas de sus librerías cliente, pongas lo
que pongas.
Dos arreglos que quitan el mensaje y abren la tabla
Desactivar Row Level Security en la tabla, y enviar la petición con la clave secreta. Los dos hacen la consulta rápida porque los dos se saltan la política, y los dos entregan las filas a gente que nunca debió tenerlas.
Desactivarla. Sin política que ejecutar, no se comprueba nada por fila y el
timeout desaparece. Supabase imprime su propia advertencia junto a la prueba
que lo sugiere: desactivar Row Level Security expone cada fila de la tabla a
cualquier rol con el permiso correspondiente. Su guía también dice que, en
proyectos existentes, una tabla nueva en public empieza con select,
insert, update y delete concedidos a anon, el rol de un visitante sin
sesión. Sin la política, esos permisos son lo único que hay entre ese visitante
y la tabla.
La clave secreta. La guía de Supabase dice que el rol service_role que
hay detrás también tiene bypassrls, así que no se ejecuta ninguna política.
Esa clave va en un servidor, y una enviada desde el navegador es una clave que
cualquiera puede leer en tu sitio;
qué claves son seguras en un frontend
repasa el par. Tampoco da mucho tiempo, porque la guía le da a service_role
los mismos 8 segundos que a authenticator.
Si alguna tabla tuya responde a una petición sin ningún inicio de sesión es una de las nueve comprobaciones del escaneo gratuito de Reeve. Lee tu sitio en producción como lo ve un visitante, tarda unos 20 segundos y no necesita cuenta.
Que la política siga rápida y la tabla cerrada
Una política que arreglaste hoy tendrá una versión siguiente. Tu builder escribe políticas cada vez que una función nueva necesita una, así que la siguiente puede volver a llamar a una función por fila, o abrir la tabla para que cargue una pantalla.
Reeve Monitor revisa tu app desde fuera, cada hora:
- las nueve comprobaciones cada hora, incluida si alguna tabla responde a una petición sin inicio de sesión
- si la aplicación responde, cada 60 segundos
- un mensaje cuando un resultado cambia, para que una tabla que empieza a responder a desconocidos no espere a que mires
- un informe mensual de lo que vio
Reeve Care guarda una copia de tu base de datos de Supabase de antes de tu cambio:
- una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
- cada copia verificada antes de contar, contando las filas de cada tabla
- una restauración en un clic que guarda lo que hay ahora antes de reemplazarlo
- también tus archivos subidos, en cuanto conectes una credencial de Storage
- todo lo que hace Monitor
Qué hacer hoy
Qué hacer
- Busca
statement timeouten los logs de Postgres y anota la consulta y el rol que la envió. - Mide esa consulta con
explain analyzecomoanonoauthenticated, los roles que usa tu app, y leeExecution TimeyRows Removed by Filter. - Envuelve en
(select …)cada llamada a función de las políticas de esa tabla, incluidos tus propios helpers que el Performance Advisor no señala. - Indexa la columna que compara cada política, y añade
to authenticateda cada política pensada solo para usuarios con sesión. - Sube un límite al final, en la única función que lo necesita, y deja Row Level Security activada.
Empieza por la tabla que hay detrás de la página que falló. Si todavía no tiene políticas propias, activar Row Level Security en cada tabla tiene las tres formas que necesitan la mayoría de las tablas.
Preguntas frecuentes
¿Qué significa "canceling statement due to statement timeout" en Supabase?
Postgres detuvo tu consulta porque duró más de lo que permite el rol que la envió, y deshizo lo que había hecho. Supabase da 3 segundos a un visitante sin sesión y 8 a un usuario con sesión. El error lleva el código 57014, que Postgres usa también para una consulta cancelada a petición, así que lee la frase además del número.
¿Por qué mi consulta funciona en el SQL Editor pero da timeout en mi app?
Porque el SQL Editor trabaja con el rol postgres y tu app no. Supabase no le da a postgres un límite propio, y el rol tiene bypassrls, así que el editor nunca ejecuta tus políticas de Row Level Security. Tu app funciona como anon o authenticated, con un límite de 3 u 8 segundos y cada política comprobada en cada fila que lee la consulta.
¿Puede Row Level Security causar un statement timeout?
Sí. Postgres comprueba la política en cada fila que lee la consulta, y una llamada a función dentro de la política se ejecuta otra vez por cada una. En mediciones que Supabase publicó para una tabla de 100.000 filas, una política que llamaba a un helper is_admin() tardó 11.000 milisegundos, y la misma política con la llamada envuelta como (select is_admin()) tardó 7.
¿Cómo aumento el statement timeout en Supabase?
Para un trabajo lento concreto, ponlo en la función que lo ejecuta con alter function … set statement_timeout. Para un rol entero, ejecuta alter role con el nuevo statement_timeout y después notify pgrst, 'reload config' para que la Data API lo recoja. Supabase limita a 60 segundos las consultas de sus librerías cliente. Arregla antes la política, porque el límite de anon lo comparten todos los visitantes sin sesión.
¿Debería desactivar RLS para que la consulta vaya más rápido?
No. La consulta va más rápido porque no se ejecuta ninguna política, y la tabla queda abierta para cualquier rol con permiso sobre ella. La guía de Supabase dice que, en proyectos existentes, una tabla nueva en public empieza con select, insert, update y delete concedidos a anon, el rol de un visitante sin sesión. Envuelve las llamadas a funciones y añade un índice.