Основи безпеки
Supabase "canceling statement due to statement timeout": пояснення
Supabase "canceling statement due to statement timeout": запит перевищив ліміт своєї ролі, 3 секунди для відвідувача. Спершу перевірте політику RLS.

Коротко
- "canceling statement due to statement timeout" у Supabase означає, що Postgres зупинив запит, бо той тривав довше, ніж дозволено ролі, яка його надіслала: 3 секунди для відвідувача без входу, 8 для того, хто увійшов. Нічого з того, що робив запит, не зберігається.
- Той самий запит працює в SQL Editor, бо редактор працює від ролі postgres, яка не має власного ліміту й повністю оминає ваші політики Row Level Security. Ваш застосунок перевіряє кожну політику на кожному рядку, який читає.
- Виправте політику, перш ніж чіпати ліміт: обгорніть виклики функцій у (select …), додайте індекс на колонку, яку порівнює політика, і назвіть роль. Вищий ліміт для відвідувачів дає кожному незнайомцеві більше часу з вашою базою.
Сторінка вашого застосунку завантажує список, індикатор крутиться кілька секунд, а потім нічого. У консолі браузера, або в червоному повідомленні, яке додав ваш білдер, є один рядок:
canceling statement due to statement timeout
Ви відкриваєте ту саму таблицю в Supabase, і всі рядки на місці. Питаєте білдер, у якому зробили застосунок (Lovable, Bolt, v0, Cursor, Claude Code чи Replit), який запит надсилає ця сторінка, вставляєте його в SQL Editor, і він повертається за мить.
Перша порада, яку ви знайдете для "canceling statement due to statement timeout" у Supabase, звучить так: підніміть ліміт. Цей ліміт робить свою роботу, і його ділить кожен відвідувач вашого застосунку. Перш ніж щось піднімати, подивіться на політику Row Level Security цієї таблиці, тобто на правило, яке Postgres перевіряє, перш ніж віддати кожен рядок. У вимірюваннях самого Supabase саме там запит, який мав би тривати 7 мілісекунд, витрачає 11 секунд.
Що означає "canceling statement due to statement timeout" у Supabase?
Postgres, база даних усередині вашого проєкту Supabase, зупинив запит, бо той тривав довше, ніж дозволено ролі, яка його надіслала, і скасував те, що запит устиг зробити.
Речення походить від самого Postgres, з кодом помилки 57014, який Postgres
називає query_canceled. Supabase передає його вашому застосунку без змін,
тож помилка, яку отримує ваш код, містить code: '57014' і це речення як
повідомлення. Postgres використовує той самий код для canceling statement due to user request, тобто скасування, про яке хтось попросив, тому речення
каже вам більше, ніж число.
Уявіть свою базу як бібліотеку зі стійкою видачі та кухонним таймером. Щоразу, коли хтось просить книжки, бібліотекарка заводить таймер, перш ніж іти до полиць, і скільки він триває, залежить від того, хто питає. Коли він дзвенить, кожна книжка повертається на полицю, а відвідувач отримує папірець із цим реченням. Якщо запит мав щось змінити, зміну теж повертають назад.
Чому запит працює в SQL Editor і падає в моєму застосунку?
Бо SQL Editor питає як інший відвідувач, у якого немає власного таймера і немає правил для перевірки.
Кожен запит доходить до Postgres під певною роллю, тобто іменем, яке вирішує, що цьому запиту можна. Посібник Supabase про тайм-аути наводить ліміт, з яким стартує кожна роль:
| Хто питає | Роль | Дозволений час |
|---|---|---|
| Відвідувач вашого застосунку без входу | anon | 3 секунди |
| Користувач вашого застосунку, який увійшов | authenticated | 8 секунд |
| Ваш сервер із секретним ключем | service_role | власного немає, тож діють 8 секунд ролі authenticator |
| Ви, у дашборді та в SQL Editor | postgres | власного немає, обмежено типовим значенням у 2 хвилини |
Роль редактора має й другу перевагу. Посібник Supabase про Row Level Security
каже, що в Supabase postgres має bypassrls, атрибут Postgres, який оминає
кожну політику. Тож редактор відповідає, не прочитавши жодного правила. Запит
вашого застосунку читає кожну політику таблиці на кожному рядку, який
переглядає, з таймером, що дзвенить через три секунди для відвідувача без
входу. Редактор заміряв інший запит, ніж той, що надсилає ваш застосунок.
У бібліотеці ви належите до персоналу. Ви заходите до сховища без таймера, і ніхто не питає вашого читацького квитка. Відвідувач має три секунди, а бібліотекарка дорогою перевіряє правила на кожній книжці.
Знайдіть скасований запит
Пошукайте в логах Postgres свого проєкту слова statement timeout і запишіть,
яка роль надіслала запит.
Посібник Supabase про тайм-аути має для цього запит. У дашборді відкрийте Explorer, оберіть Run SQL, поставте джерелом Logs, оберіть проміжок часу, що охоплює збій, і запустіть цю скорочену версію:
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;
user_name, що дорівнює authenticator, означає, що запит прийшов через Data
API, сервер, через який проходить кожен виклик supabase.from(…) вашого
застосунку. Таблиця ролей у посібнику вказує authenticator як PostgREST, а
це і є той сервер. Колонка query показує SQL, на який перетворився запит
вашого застосунку, і назва таблиці в ньому вказує, яку таблицю дивитися.
Сторінка Query Performance у дашборді є іншим місцем, куди зазвичай дивляться, і посібник попереджає, що вона показує лише повільні запити, які завершилися. Скасований запит є в логах.
Якщо логи показують скасування на всіх таблицях одночасно, повільна частина не в одній політиці, і відрізнити збій Supabase від проблеми у вашому застосунку допоможе почати.
Заміряйте його так, як виконує ваш застосунок
Запустіть запит від ролі свого застосунку з explain analyze попереду, і
Postgres виведе, скільки часу він зайняв і скільки рядків відкинула політика.
Це метод із посібника Supabase про продуктивність Row Level Security, загорнутий
у begin і rollback, щоб зміна ролі закінчилася разом із блоком. Скопіюйте
id справжнього користувача з Authentication → Users замість нулів:
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;
Важать два рядки виводу. Execution Time є числом, яке ви порівнюєте з 3 і 8
секундами. Rows Removed by Filter рахує рядки, які Postgres прочитав, а
політика відкинула, і посібник Supabase називає його сигналом, за яким варто
стежити: політика, що на кожному читанні відсіює більшу частину таблиці, є
політикою, чия колонка фільтра потребує індексу.
Щоб заміряти, що отримує відвідувач без входу, напишіть set local role anon;
і приберіть рядок із claims.
Той самий посібник пропонує запустити запит раз з увімкненим Row Level Security і раз із вимкненим, щоб побачити, чи вартість у політиці. Він також каже робити це лише поза продакшеном, бо вимкнення відкриває кожен рядок. На робочому застосунку порівнюйте натомість політику з її переписаною версією.
Як політика може коштувати секунд?
Postgres перевіряє політику на кожному рядку, який читає запит, і виклик функції в політиці виконується знову для кожного з цих рядків.
Supabase опублікував вимірювання цього на таблиці зі 100 000 рядків. Ось чотири з них, повільний і швидкий спосіб:
| Що перевіряє політика | Записано як | Тривало | Переписано як | Тривало |
|---|---|---|---|---|
| Рядки, що належать користувачеві, який увійшов | auth.uid() = user_id | 179 мс | (select auth.uid()) = user_id | 9 мс |
| Те саме правило, без індексу на колонці | auth.uid() = user_id | 171 мс | те саме, з індексом на user_id | менше 0,1 мс |
| Перевірка адміна, яка читає іншу таблицю | is_admin() | 11 000 мс | (select is_admin()) | 7 мс |
| Роль, яку шукають в іншій таблиці | has_role() = role | 178 000 мс | (select has_role()) = role | 12 мс |
Перші два повільні, і все ж на цій таблиці вкладаються в три секунди. Останні два виходять за будь-який ліміт, що є в запиту застосунку. Допоміжна функція, яка читає другу таблицю, з тих, що пишуть, аби перевірити, чи хтось адмін, викликається раз на кожен рядок першої, тож на цій таблиці вона прочитала другу 100 000 разів.
У бібліотеці така функція схожа на бібліотекарку, яка ходить до офісу біля
входу спитати, чи ви з персоналу, раз на кожну книжку на полиці. Обгорнути її в
(select …) означає спитати один раз і записати відповідь на папірці.
Performance Advisor у Supabase ловить частину цього. Його правило з назвою
Auth RLS Initialization Plan позначає політику, яка "re-evaluates
current_setting() or auth.is_admin(), під цей опис не підпадає, тож сторінка advisor
без попереджень не перевіряла ваших функцій. Перегляньте політики таблиці самі
й пошукайте будь-який виклик функції, який не обгорнуто.
Виправляйте в такому порядку
Обгорніть виклики функцій, додайте індекс на колонку, яку порівнює політика, потім назвіть роль. Кожен крок є коротким шматком SQL, який можна запустити в редакторі або передати своєму білдеру.
1. Обгорніть кожен виклик функції в (select …).
alter policy "read own orders" on public.orders
using ( (select auth.uid()) = user_id );
alter policy зберігає назву і роль політики та замінює умову. Візьміть назву
своєї політики і колонку своєї таблиці, і зробіть те саме з допоміжною
функцією: (select private.is_admin()). Supabase ставить одну умову: це працює
лише для значення, яке не змінюється від рядка до рядка. Чому дужки мають
значення, пояснює
посібник з увімкнення Row Level Security,
а допоміжна функція, написана, щоб
позбутися помилки нескінченної рекурсії,
саме з тих, яким вони потрібні.
2. Додайте індекс на колонку, яку порівнює політика.
create index orders_user_id_idx on public.orders (user_id);
Посібник Supabase додає застереження. Колонка вважається проіндексованою лише
тоді, коли стоїть в індексі першою, тож таблиця з первинним ключем
(team_id, user_id) не має індексу на user_id. На великій таблиці, у яку
пишуть, поки ви працюєте, документація Postgres каже, що звичайний
create index блокує запис у таблицю, доки індекс не збудовано.
create index concurrently будує його без цього блокування, і його треба
запускати окремою командою, поза транзакцією.
3. Назвіть роль, для якої політика. Політика, записана з to authenticated,
ніколи не виконується для відвідувача без входу. У вимірюваннях Supabase запит
без входу до політики без to тривав 170 мс, а з to authenticated менше 0,1
мс.
4. Фільтруйте і в самому запиті. Політика працює як невидимий where у
кожному запиті, і посібник Supabase радить повторити його в запиті вашого
застосунку, .eq('user_id', userId), бо так Postgres може спланувати швидший
запит. У їхніх вимірюваннях це скоротило запит із 171 мс до 9.
5. Приберіть join із командної політики. Політика, що звіряє кожен рядок із таблицею учасників, робить цей пошук для кожного рядка:
using ( (select auth.uid()) in (
select user_id from team_user where team_user.team_id = projects.team_id
) )
Посібник Supabase переписує її так, щоб вона прочитала команди один раз і порівнювала з цим списком:
using ( team_id in (
select team_id from team_user where user_id = (select auth.uid())
) )
Потім знову запустіть explain analyze з початку і порівняйте
Execution Time та Rows Removed by Filter із тим, що було раніше.
Чи варто збільшувати statement timeout?
Лише для запиту, який ви заміряли, переписали і який досі потрібен, і тоді для цієї однієї функції, а не для цілої ролі.
Ліміти задумані навмисно. Посібник Supabase з імпорту даних каже, що вони
існують, щоб чесно розподіляти ресурси й не давати довгим запитам впливати на
всю систему. Ліміт anon спільний для всіх відвідувачів без входу, і будь-хто
може надіслати цей запит із публічним ключем, що йде всередині вашого
застосунку. Підняти його з трьох секунд до тридцяти означає дати кожному з цих
запитів тридцять секунд вашої бази.
Коли задача справді триває довше, дайте ліміт функції, яка її виконує.
Посібник Supabase описує це для регулярних функцій, яким потрібен виняток щодо
часу виконання, і це працює для функції, яку ваш застосунок викликає через
supabase.rpc(…):
alter function public.monthly_totals() set statement_timeout to '20s';
Якщо ви все ж змінюєте роль, команди з посібника такі:
alter role authenticated set statement_timeout = '15s';, а потім
notify pgrst, 'reload config';, щоб Data API підхопив нове значення.
Supabase обмежує запити зі своїх клієнтських бібліотек 60 секундами, хоч би що
ви задали.
Два рішення, які прибирають повідомлення і відкривають таблицю
Вимкнути Row Level Security на таблиці, і надіслати запит із секретним ключем. Обидва роблять запит швидким, бо обидва оминають політику, і обидва віддають рядки людям, яким ви їх ніколи не призначали.
Вимкнути. Без політики нічого не перевіряється для кожного рядка, і
тайм-аут зникає. Supabase друкує власне застереження поруч із тестом, який це
пропонує: вимкнення Row Level Security відкриває кожен рядок таблиці для
будь-якої ролі з відповідним правом. Його посібник також каже, що в наявних
проєктах нова таблиця в public одразу має select, insert, update і
delete, надані anon, ролі відвідувача без входу. Коли політики немає, ці
права і є всім, що стоїть між таким відвідувачем і таблицею.
Секретний ключ. Посібник Supabase каже, що роль service_role, яка стоїть
за ним, теж має bypassrls, тож жодна політика не виконується. Цьому ключу
місце на сервері, а ключ, надісланий із браузера, може прочитати з вашого
сайту будь-хто;
які ключі безпечні у фронтенді
розбирає цю пару. Часу він теж майже не додає, бо посібник дає service_role
ті самі 8 секунд, що й authenticator.
Чи відповідає якась ваша таблиця на запит зовсім без входу, перевіряє одна з дев’яти перевірок безкоштовного сканування Reeve. Воно читає ваш робочий сайт так, як його бачить відвідувач, триває близько 20 секунд і не потребує облікового запису.
Щоб політика лишалася швидкою, а таблиця закритою
Політика, яку ви виправили сьогодні, матиме наступну версію. Ваш білдер пише політики щоразу, коли вони потрібні новій функції, тож наступна може знову викликати функцію для кожного рядка або відкрити таблицю, щоб завантажився екран.
Reeve Monitor перевіряє ваш застосунок ззовні, щогодини:
- усі дев’ять перевірок щогодини, зокрема чи відповідає якась таблиця на запит без входу
- чи працює застосунок, кожні 60 секунд
- повідомлення, коли результат змінюється, щоб таблиця, яка почала відповідати незнайомцям, не чекала, поки ви глянете
- місячний звіт про те, що він побачив
Reeve Care тримає копію вашої бази Supabase з часу до вашої зміни:
- зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
- кожна копія перевірена, перш ніж її зарахують, підрахунком рядків у кожній таблиці
- відновлення в один клік, яке зберігає те, що є зараз, перш ніж замінити
- ваші завантажені файли теж, щойно ви приєднаєте доступ до Storage
- усе, що робить Monitor
Що зробити сьогодні
Що робити
- Пошукайте
statement timeoutу логах Postgres і запишіть запит та роль, яка його надіслала. - Заміряйте цей запит через
explain analyzeвідanonабоauthenticated, ролей, які використовує ваш застосунок, і прочитайтеExecution TimeтаRows Removed by Filter. - Обгорніть у
(select …)кожен виклик функції в політиках цієї таблиці, зокрема власні допоміжні функції, яких Performance Advisor не позначає. - Додайте індекс на колонку, яку порівнює кожна політика, і допишіть
to authenticatedдо кожної політики, призначеної лише для користувачів, які увійшли. - Піднімайте ліміт останнім, на тій одній функції, якій він потрібен, і лишайте Row Level Security увімкненим.
Почніть із таблиці за сторінкою, яка не завантажилася. Якщо в неї ще немає власних політик, увімкнення Row Level Security на кожній таблиці дає три форми, яких потребує більшість таблиць.
Поширені запитання
Що означає "canceling statement due to statement timeout" у Supabase?
Postgres зупинив ваш запит, бо той тривав довше, ніж дозволено ролі, яка його надіслала, і скасував те, що запит устиг зробити. Supabase дає 3 секунди відвідувачеві без входу і 8 користувачеві, який увійшов. Помилка має код 57014, який Postgres використовує і для запиту, скасованого на прохання, тож читайте речення, а не лише число.
Чому мій запит працює в SQL Editor, а в застосунку падає з тайм-аутом?
Бо SQL Editor працює від ролі postgres, а ваш застосунок ні. Supabase не дає postgres власного ліміту, і ця роль має bypassrls, тож редактор ніколи не виконує ваших політик Row Level Security. Ваш застосунок працює як anon або authenticated, з лімітом 3 або 8 секунд, і кожна політика перевіряється на кожному рядку, який читає запит.
Чи може Row Level Security спричинити statement timeout?
Так. Postgres перевіряє політику на кожному рядку, який читає запит, і виклик функції в політиці виконується знову для кожного з них. У вимірюваннях, які Supabase опублікував для таблиці на 100 000 рядків, політика з викликом допоміжної функції is_admin() виконувалася 11 000 мілісекунд, а та сама політика з викликом, обгорнутим як (select is_admin()), 7.
Як збільшити statement timeout у Supabase?
Для однієї повільної задачі задайте його на функції, яка її виконує, через alter function … set statement_timeout. Для цілої ролі виконайте alter role з новим statement_timeout, а потім notify pgrst, 'reload config', щоб Data API підхопив значення. Supabase обмежує запити зі своїх клієнтських бібліотек 60 секундами. Спершу виправте політику, бо ліміт anon спільний для всіх відвідувачів без входу.
Чи вимкнути RLS, щоб запит працював швидше?
Ні. Запит стає швидшим, бо жодна політика не виконується, і таблиця тоді відкрита для будь-якої ролі з правом на неї. Посібник Supabase каже, що в наявних проєктах нова таблиця в public одразу має select, insert, update і delete, надані anon, ролі відвідувача без входу. Замість цього обгорніть виклики функцій і додайте індекс.