Odporność na przeciążenie bazy (D1)
Dokument opisuje, co się dzieje, gdy baza danych (Cloudflare D1) przez chwilę odmawia obsługi zapytań, oraz jakie mechanizmy chronią panel przed pokazaniem pracownikowi ekranu błędu.
👤 Instrukcja dla pracownika
Objaw
Strona panelu (najczęściej Dashboard ➔ Grafik) zamiast danych pokazuje ekran błędu. W zgłoszeniu wysyłanym przyciskiem „Zgłoś błąd" widnieje komunikat:
D1_ERROR: D1 DB is overloaded. Requests queued for too long.
Oznacza to, że baza danych chwilowo odrzucała zapytania — nie jest to błąd danych ani uprawnień.
Co robić
- Odśwież stronę. Takie przeciążenia trwają zwykle kilkanaście–kilkadziesiąt sekund.
- Jeśli po minucie strona nadal się nie ładuje, zgłoś błąd przyciskiem „Zgłoś błąd" — zgłoszenie zawiera godzinę, ścieżkę i identyfikator (digest) potrzebne do analizy.
- Nic nie ginie: przeciążenie dotyczy odczytu danych, a operacje zapisu (płatności, zapisy na zajęcia) albo wykonują się w całości, albo zwracają błąd — nie zostają wykonane „w połowie".
🛠 Dokumentacja techniczna
Skąd bierze się błąd
D1 DB is overloaded. Requests queued for too long. zwraca sama D1, gdy kolejka zapytań do bazy rośnie szybciej, niż baza jest w stanie ją obsłużyć. Zapytanie nie zostaje wykonane, więc jego ponowienie jest bezpieczne.
Incydent z 28.08.2026 (06:35–06:36 UTC) pokazał trzy warstwy problemu:
- Render
/dashboard/schedulenie miał żadnego ponowienia — pierwszy odrzuconySELECTkończył się ekranem błędu Server Components. - Odczyt sesji (
getServerSession) ponawiał próby w oknie ~0,65 s, czyli krócej niż trwało przeciążenie; po trzech próbach zwracałAPIError: Failed to get session. - Każdy zalogowany błąd to
INSERTdo tabelilogs— w trakcie przeciążenia logger dokładał kolejne zapytania do przepełnionej kolejki i sam się wywracał („Failed to save log to database"), przez co incydent nie zapisał się w bazie logów.
Mechanizmy
withD1Retry (lib/d1-retry.ts) — ponawia operację przy błędzie rozpoznanym jako przejściowy (lib/d1-transient.ts: przeciążenie, zapchana kolejka, restart isolate po deployu). Opóźnienia: 200/600/1500/3000 ms, każde z jitterem ±50%, czyli maksymalnie 5 prób.
Wspólne okno ponowień (RETRY_BUDGET_MS = 6 s) — wszystkie ponowienia w obrębie jednego renderu czerpią z tego samego budżetu, otwieranego przy pierwszym przejściowym błędzie. Bez tego strona, która czeka kolejno na kilka grup zapytań (/dashboard/schedule ma dwie plus odczyt sesji), mnożyłaby budżet i kazała pracownikowi patrzeć na pustą kartę przez ~16 s, zanim pokaże błąd. Okno jest per-request dzięki React.cache; poza renderem serwerowym (crony, workery) każda operacja dostaje własne okno — tam nikt nie czeka.
Ponowienia logowane są przez logDebug, czyli tylko na konsolę. WARN trafiłby do tabeli logs, czyli zapisem do tej samej bazy, która właśnie odrzuca zapytania.
getRetryingDb() / withReadRetry() — zwraca D1Database opakowaną w Proxy. Statementy, których SQL zaczyna się od SELECT/WITH/PRAGMA/EXPLAIN i nie zawiera słowa kluczowego modyfikującego dane, dostają ponowienie na all(), first(), raw() i run(). Zapisy przechodzą bez zmian — ponowienie INSERT-a lub UPDATE-a mogłoby zdublować operację, więc write kończy się błędem, a decyzję o powtórzeniu podejmuje kod wyższego poziomu.
Moduły korzystające z getRetryingDb() (pełna ścieżka renderowania grafiku): game, court, users-db, holidays, activity-types, app-settings, cash-management. Finanse i płatności używają withD1Retry punktowo.
Odczyt sesji (lib/session.ts) — getServerSession ponawia przy błędzie przejściowym D1 oraz przy APIError 5xx z Better Auth, z opóźnieniami 150/500/1500/3000 ms.
Logger (lib/logger.ts) — gdy zapis logu do D1 padnie z powodu przeciążenia, zapisy do bazy są wstrzymywane na 30 sekund (DB_WRITE_PAUSE_MS). W czasie pauzy:
INFO/WARNnie są zapisywane (nadal idą naconsole, czyli do logów Workera),ERRORjest zapisywany, ale najwyżej raz na 5 sekund (PAUSED_ERROR_WRITE_INTERVAL_MS) — awaria to moment, w którym tabelalogsmusi przyjmować błędy, a nie moment na ciszę,- po wygaśnięciu pauzy pierwszy zapis poprzedzony jest wierszem
Log persistence paused while D1 was overloadedz liczbą pominiętych wpisów, żeby luka w logach była widoczna zamiast wyglądać jak „nic się nie działo".
Diagnostyka incydentu
Logi aplikacji z okna awarii szukaj w dwóch miejscach:
npx wrangler d1 execute dev --remote --command \
"SELECT level, context, message, created_at FROM logs WHERE created_at >= '2026-08-28T06:20' ORDER BY created_at DESC LIMIT 50"
Jeżeli tabela logs jest pusta w oknie awarii, to sam objaw przeciążenia — logi z tego okna nie miały jak się zapisać. Wtedy jedynym źródłem są logi Workera (observability w wrangler.admin.toml, panel Cloudflare).
Czego to nie rozwiązuje
Ponowienia maskują przeciążenia trwające sekundy — dokładnie tyle, ile mieści wspólne okno 6 s. Nie pomogą, gdy baza jest przeciążona minutami — wtedy trzeba szukać źródła obciążenia (crony, zapytania pełnoskanowe, rozmiar tabeli logs, która nie ma retencji).