Skip to main content

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ć

  1. Odśwież stronę. Takie przeciążenia trwają zwykle kilkanaście–kilkadziesiąt sekund.
  2. 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.
  3. 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:

  1. Render /dashboard/schedule nie miał żadnego ponowienia — pierwszy odrzucony SELECT kończył się ekranem błędu Server Components.
  2. 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.
  3. Każdy zalogowany błąd to INSERT do tabeli logs — 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/WARN nie są zapisywane (nadal idą na console, czyli do logów Workera),
  • ERROR jest zapisywany, ale najwyżej raz na 5 sekund (PAUSED_ERROR_WRITE_INTERVAL_MS) — awaria to moment, w którym tabela logs musi przyjmować błędy, a nie moment na ciszę,
  • po wygaśnięciu pauzy pierwszy zapis poprzedzony jest wierszem Log persistence paused while D1 was overloaded z 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).