Timeouty kontekstu w Go dla API: od handlerów HTTP do SQL
Timeouty kontekstu w Go pozwalają przekazać deadline od handlera HTTP do wywołań SQL, zapobiec zablokowanym żądaniom i utrzymać stabilność usług pod obciążeniem.

Dlaczego żądania utkną (i dlaczego to szkodzi pod obciążeniem)
Żądanie „utknie”, gdy czeka na coś, co nie zwraca: wolne zapytanie do bazy, zablokowane połączenie z puli, problem z DNS lub serwis zewnętrzny, który przyjmuje wywołanie, ale nie odpowiada.
Objaw jest prosty: niektóre żądania trwają w nieskończoność, a za nimi gromadzą się kolejne. Często zobaczysz rosnącą pamięć, wzrost liczby goroutine oraz kolejkę otwartych połączeń, która nigdy się nie opróżnia.
Pod obciążeniem utkniete żądania szkodzą podwójnie. Zajmują workerów i trzymają rzadkie zasoby, takie jak połączenia do bazy czy blokady. To sprawia, że normalnie szybkie żądania stają się wolne, co zwiększa nakładanie się pracy i jeszcze bardziej wydłuża oczekiwanie.
Retry i nagłe skoki ruchu pogarszają spiralę. Klient przerywa i próbuje ponownie, podczas gdy oryginalne żądanie nadal trwa, więc opłacasz teraz dwa żądania. Pomnóż to przez wielu klientów podczas krótkiego spowolnienia i możesz przeciążyć bazę lub osiągnąć limity połączeń, nawet jeśli średni ruch jest OK.
Timeout to po prostu obietnica: „nie będziemy czekać dłużej niż X”. Pomaga szybko zawieść i zwolnić zasoby, ale nie sprawi, że praca zakończy się szybciej.
Nie gwarantuje też natychmiastowego zatrzymania pracy. Na przykład baza danych może dalej wykonywać zapytanie, serwis zewnętrzny może zignorować anulowanie, albo twój kod może nie być bezpieczny przy anulowaniu.
Czego timeout na pewno zapewnia: handler może przestać czekać, zwrócić przejrzysty błąd i zwolnić to, co zajmuje. To ograniczone oczekiwanie powstrzymuje kilka wolnych wywołań przed zamianą w pełny outage.
Celem stosowania timeoutów w Go jest jeden wspólny deadline od krawędzi do najgłębszego wywołania. Ustal go raz na granicy HTTP, przekaż ten sam kontekst przez kod serwisu i użyj go w wywołaniach database/sql, żeby baza też wiedziała, kiedy przestać czekać.
Kontekst w Go prostymi słowami
context.Context to mały obiekt, który przekazujesz przez kod, żeby opisać, co się teraz dzieje. Odpowiada na pytania: „czy to żądanie jest nadal ważne?”, „kiedy mamy się poddać?” i „jakie małe wartości zakresu żądania powinny iść razem z tą pracą?”.
Duża korzyść jest taka, że jedna decyzja na brzegu systemu (handler HTTP) może chronić każdy kolejny krok, o ile dalej przekażesz ten sam kontekst.
Co niesie kontekst
Kontekst to nie miejsce na dane biznesowe. To sygnały sterujące i niewielkie metadane zakresu żądania: anulowanie, deadline/timeout i drobne informacje jak request ID do logów.
Różnica timeout vs anulowanie jest prosta: timeout to jedna z przyczyn anulowania. Jeśli ustawisz timeout 2 sekundy, kontekst zostanie anulowany po upływie tych 2 sekund. Ale kontekst można też anulować wcześniej, jeśli użytkownik zamknie kartę, load balancer rozłączy połączenie lub twój kod zdecyduje przerwać przetwarzanie.
Kontekst przepływa przez wywołania funkcji jako jawny parametr, zwykle pierwszy: func DoThing(ctx context.Context, ...). O to chodzi — trudniej go "zapomnieć", gdy widać go w każdym miejscu wywołania.
Gdy deadline wygaśnie, wszystko co obserwuje ten kontekst powinno szybko przestać. Na przykład zapytanie do bazy używające QueryContext powinno zwrócić wcześniej z błędem takim jak context deadline exceeded, a handler może odpowiedzieć timeoutem zamiast wisieć, aż serwer skończy workery.
Dobry model mentalny: jedno żądanie, jeden kontekst, przekazywany wszędzie. Gdy żądanie umiera, praca powinna umrzeć razem z nim.
Ustalanie jasnego deadline na granicy HTTP
Jeśli chcesz, żeby timeouty end-to-end działały, zdecyduj, kiedy zegar startuje. Najbezpieczniej jest zrobić to tuż na brzegu HTTP, aby każde kolejne wywołanie (logika biznesowa, SQL, inne serwisy) odziedziczyło ten sam deadline.
Możesz ustawić ten deadline w kilku miejscach. Timeouty na poziomie serwera to dobre minimum i chronią przed wolnymi klientami. Middleware jest świetne dla spójności między grupami tras. Ustawienie go wewnątrz handlera też jest OK, gdy chcesz czegoś jawnego i lokalnego.
Dla większości API timeouty per-request w middleware lub handlerze są najłatwiejsze do zrozumienia. Niech będą realistyczne: użytkownicy wolą szybkie, jasne niepowodzenie niż wiszące żądanie. Wiele zespołów używa krótszych budżetów dla odczytów (1–2s) i trochę dłuższych dla zapisów (3–10s), w zależności od tego, co robi endpoint.
Oto prosty wzorzec handlera:
func (s *Server) getReport(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
report, err := s.reports.Generate(ctx, r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusGatewayTimeout)
return
}
json.NewEncoder(w).Encode(report)
}
Dwie zasady utrzymują to skuteczne:
- Zawsze wywołuj
cancel(), żeby timery i zasoby były szybko zwolnione. - Nigdy nie zastępuj kontekstu żądania
context.Background()lubcontext.TODO()wewnątrz handlera. To łamie łańcuch i wywołania do bazy albo zewnętrznych usług mogą działać w nieskończoność nawet po odejściu klienta.
Propagowanie kontekstu przez bazę kodu
Gdy ustawisz deadline na brzegu HTTP, prawdziwa praca polega na upewnieniu się, że ten sam deadline dociera do każdej warstwy, która może blokować. Pomysł to jeden zegar dzielony przez handler, logikę serwisu i wszystko, co dotyka sieci lub dysku.
Prosta zasada utrzymuje spójność: każda funkcja, która może czekać, powinna przyjmować context.Context i powinien to być pierwszy parametr. To sprawia, że jest to oczywiste w miejscach wywołań i staje się nawykiem.
Praktyczny wzorzec sygnatury
Preferuj sygnatury typu DoThing(ctx context.Context, ...) dla serwisów i repozytoriów. Unikaj ukrywania kontekstu w strukturach lub tworzenia go od nowa z context.Background() w niższych warstwach, bo to cicho odcina deadline wywołującego.
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := h.svc.CreateOrder(ctx, r.Body); err != nil {
// map context errors to a clear client response elsewhere
http.Error(w, err.Error(), http.StatusRequestTimeout)
return
}
}
func (s *Service) CreateOrder(ctx context.Context, body io.Reader) error {
// parsing or validation can still respect cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return s.repo.InsertOrder(ctx, /* data */)
}
Czyste obsługiwanie wczesnych zakończeń
Traktuj ctx.Done() jako normalną ścieżkę kontrolną. Dwa nawyki pomagają:
- Sprawdzaj
ctx.Err()przed rozpoczęciem kosztownej pracy i po długich pętlach. - Zwracaj
ctx.Err()w górę bez zmiany, aby handler mógł szybko odpowiedzieć i przestać marnować zasoby.
Gdy każda warstwa przekazuje ten sam ctx, jeden timeout może przerwać analizę, logikę biznesową i oczekiwania bazy w jednym kroku.
Stosowanie deadline’ów do zapytań database/sql
Gdy handler HTTP ma deadline, upewnij się, że praca z bazą rzeczywiście go słucha. W database/sql oznacza to używanie metod z obsługą kontekstu za każdym razem. Jeśli wywołasz Query() lub Exec() bez kontekstu, twoje API może dalej czekać na wolne zapytanie nawet po tym, jak klient się podda.
Używaj konsekwentnie: db.QueryContext, db.QueryRowContext, db.ExecContext i db.PrepareContext (następnie QueryContext/ExecContext na zwróconym prepared statement).
func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, email FROM users WHERE id = $1`, id,
)
var u User
if err := row.Scan(&u.ID, &u.Email); err != nil {
return nil, err
}
return &u, nil
}
func (s *Store) UpdateEmail(ctx context.Context, id int64, email string) error {
_, err := s.db.ExecContext(ctx,
`UPDATE users SET email = $1 WHERE id = $2`, email, id,
)
return err
}
Dwie rzeczy łatwo przeoczyć.
Po pierwsze, twój driver SQL musi respektować anulowanie kontekstu. Wiele to robi, ale potwierdź to w swoim stacku, testując celowo powolne zapytanie i sprawdzając, że odwołuje się szybko po przekroczeniu deadline’u.
Po drugie, rozważ timeout po stronie bazy jako zabezpieczenie. Na przykład Postgres może egzekwować limit na pojedyncze zapytanie (statement timeout). To chroni bazę nawet jeśli aplikacja zapomni gdzieś przekazać kontekst.
Gdy operacja kończy się timeoutem, traktuj to inaczej niż zwykły błąd SQL. Sprawdzaj errors.Is(err, context.DeadlineExceeded) i errors.Is(err, context.Canceled) i zwracaj jasną odpowiedź (np. 504) zamiast traktować to jako „baza jest zepsuta”. Jeśli generujesz backendy Go (np. z AppMaster), wyróżnianie tych ścieżek błędów upraszcza logi i retry.
Wywołania zewnętrzne: klienci HTTP, cache i inne serwisy
Nawet gdy handler i zapytania SQL respektują kontekst, żądanie nadal może wisieć, jeśli wywołanie zewnętrzne czeka wiecznie. Pod obciążeniem kilka zawieszonych goroutine potrafi się nagromadzić, zjeść pule połączeń i zamienić drobne spowolnienie w pełny outage. Naprawa to spójna propagacja plus twardy backstop.
Wychodzące HTTP
Przy wywołaniu innego API buduj request z tym samym kontekstem, żeby deadline i anulowanie przepłynęły automatycznie.
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { /* handle */ }
resp, err := httpClient.Do(req)
Nie polegaj tylko na kontekście. Skonfiguruj też klienta HTTP i transport, żeby być chronionym, jeśli kod przypadkowo użyje background context lub jeśli DNS/TLS/idle connections zablokują. Ustaw http.Client.Timeout jako górną granicę dla całego wywołania, ustaw timeouty transportu (dial, TLS handshake, response header) i używaj jednego współdzielonego klienta zamiast tworzyć nowy dla każdego żądania.
Cache i kolejki
Cache, brokery wiadomości i klienci RPC często mają własne punkty oczekiwania: zdobycie połączenia, oczekiwanie na odpowiedź, blokada na pełnej kolejce lub czekanie na lock. Upewnij się, że te operacje akceptują ctx, i używaj timeoutów na poziomie biblioteki tam, gdzie są dostępne.
Praktyczna zasada: jeśli użytkownikowi pozostało 800ms, nie zaczynaj wywołania, które może zająć 2s. Pomiń je, zastosuj degradację lub zwróć częściową odpowiedź.
Zdecyduj wcześniej, co timeout znaczy dla twojego API. Czasem poprawną odpowiedzią jest szybki błąd. Czasem częściowe dane dla opcjonalnych pól. Czasem przestarzałe dane z cache, wyraźnie oznaczone.
Jeśli budujesz backendy Go (w tym generowane, np. przez AppMaster), to różnica między „timeouty istnieją” a „timeouty spójnie chronią system” przy skokach ruchu polega na konsekwentnym stosowaniu tych zasad.
Krok po kroku: refaktoryzacja API dla timeoutów end-to-end
Refaktoryzacja pod timeouty sprowadza się do jednego nawyku: przekaż ten sam context.Context od krawędzi HTTP aż do każdego wywołania, które może blokować.
Praktyczny sposób pracy jest od góry do dołu:
- Zmień handler i główne metody serwisowe, by przyjmowały
ctx context.Context. - Zaktualizuj każde wywołanie DB, by używało
QueryContextlubExecContext. - Zrób to samo dla wywołań zewnętrznych (klienci HTTP, cache, kolejki). Jeśli biblioteka nie akceptuje
ctx, opakuj ją lub zastąp. - Zdecyduj, kto zarządza timeoutami. Często zasada jest: handler ustawia ogólny deadline; niższe warstwy ustawiają krótsze deadliny tylko tam, gdzie to potrzebne.
- Spraw, by błędy były przewidywalne na brzegu: mapuj
context.DeadlineExceededicontext.Canceledna czytelne odpowiedzi HTTP.
Oto kształt, którego chcesz w całych warstwach:
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
order, err := h.svc.GetOrder(ctx, r.PathValue("id"))
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(order)
}
func (r *Repo) GetOrder(ctx context.Context, id string) (Order, error) {
row := r.db.QueryRowContext(ctx, `SELECT id,total FROM orders WHERE id=$1`, id)
// scan...
}
Wartości timeoutów powinny być nudne i spójne. Jeśli handler ma 2 sekundy łącznie, trzymaj zapytania DB poniżej 1 sekundy, żeby zostawić miejsce na kodowanie JSON i inne prace.
Aby udowodnić, że to działa, dodaj test wymuszający timeout. Proste podejście to fikcyjna metoda repo, która blokuje się aż do ctx.Done() i zwraca ctx.Err(). Test powinien asercją potwierdzić, że handler szybko zwraca 504, a nie po sztucznie długim opóźnieniu.
Jeśli generujesz backendy Go (np. AppMaster), zasada jest ta sama: jeden kontekst żądania przewleczony wszędzie z jasnym zarządzaniem deadline.
Observability: jak udowodnić, że timeouty działają
Timeouty pomagają tylko wtedy, gdy możesz je zobaczyć. Celem jest proste: każde żądanie ma deadline, a gdy zawiedzie, potrafisz wskazać, gdzie poszedł czas.
Zacznij od logów, które są bezpieczne i użyteczne. Zamiast wypisywać całe ciała żądań, loguj wystarczająco, by połączyć punkty i wykryć wolne ścieżki: request ID (lub trace ID), czy deadline jest ustawiony i ile czasu zostało w kluczowych punktach, nazwę operacji (handler, nazwa zapytania SQL, wywołanie zewnętrzne) oraz kategorię wyniku (ok, timeout, canceled, inny błąd).
Dodaj kilka skupionych metryk, żeby zachowanie pod obciążeniem było jasne:
- Liczba timeoutów według endpointu i zależności
- Latencja żądań (p50/p95/p99)
- Żądania w locie
- Latencja zapytań do DB (p95/p99)
- Współczynnik błędów rozbity według typu
Gdy obsługujesz błędy, taguj je poprawnie. context.DeadlineExceeded zwykle oznacza, że przekroczyłeś budżet. context.Canceled często znaczy, że klient się rozłączył albo najpierw wywołał timeout upstream. Trzymaj te przypadki oddzielnie, bo naprawy są różne.
Trace’owanie: znajdź, gdzie idzie czas
Spany trace powinny płynąć tym samym kontekstem z handlera HTTP do wywołań database/sql jak QueryContext. Na przykład żądanie timeoutuje po 2s, a trace pokazuje 1.8s czekania na połączenie DB. To wskazuje na rozmiar puli lub długie transakcje, a nie tekst zapytania.
Jeśli tworzysz dashboardy wewnętrzne (timeouty według trasy, top wolnych zapytań), narzędzie no-code takie jak AppMaster może pomóc szybko to wystartować bez robienia z obserwowalności osobnego projektu inżynierskiego.
Najczęstsze błędy, które unieważniają timeouty
Większość błędów typu „czasami nadal wisi” wynika z kilku małych pomyłek.
- Resetowanie zegara w trakcie. Handler ustawia 2s deadline, ale repo tworzy nowy kontekst z własnym timeoutem (albo bez timeoutu). Teraz baza może dalej działać po odejściu klienta. Przekazuj przychodzący
ctxi dokładaj krótsze terminy tylko gdy to konieczne. - Uruchamianie goroutine, które nigdy się nie kończą. Spawnując pracę z
context.Background()(albo całkowicie porzucając ctx) sprawiasz, że będzie działać dalej po anulowaniu żądania. Przekazuj kontekst żądania do goroutine i używajselectnactx.Done(). - Deadline’y za krótkie dla rzeczywistego ruchu. 50ms może działać na laptopie, ale w produkcji podczas niewielkiego spiku polegnie, powodując retryy, dodatkowe obciążenie i mini-outage. Wybieraj timeouty na podstawie normalnej latencji z zapasem.
- Ukrywanie prawdziwego błędu. Traktowanie
context.DeadlineExceededjako zwykłego 500 utrudnia debugowanie i zachowania klientów. Mapuj go na czytelny timeout i loguj różnicę między „anulowane przez klienta” a „przekroczono deadline”. - Zostawianie zasobów otwartych przy wczesnym zakończeniu. Jeśli zwracasz wcześniej, upewnij się, że nadal
defer rows.Close()i wywołujesz cancel zcontext.WithTimeout. Wycieki rows lub wisząca praca mogą wyczerpać połączenia pod obciążeniem.
Krótki przykład: endpoint wyzwala kosztowne zapytanie raportowe. Jeśli użytkownik zamknie kartę, handler ctx zostaje anulowany. Jeśli twoje wywołanie SQL użyło nowego background contextu, zapytanie nadal leci, zajmując połączenie i spowalniając innych. Gdy przekażesz ten sam ctx do QueryContext, wywołanie DB zostanie przerwane i system szybciej wróci do zdrowia.
Szybka lista kontrolna dla niezawodnych timeoutów
Timeouty pomagają tylko gdy są spójne. Pojedyncze pominięcie może utrzymać goroutine zajętą, trzymać połączenie DB i spowolnić kolejne żądania.
- Ustaw jeden jasny deadline na krawędzi (zwykle handler HTTP). Wszystko wewnątrz żądania powinno go odziedziczyć.
- Przekazuj ten sam
ctxprzez warstwy serwisu i repo. Unikajcontext.Background()w kodzie requestowym. - Używaj metod DB z kontekstem wszędzie:
QueryContext,QueryRowContext,ExecContext. - Dołącz ten sam
ctxdo wywołań wychodzących (HTTP clients, cache, kolejki). Jeśli tworzysz kontekst potomny, niech będzie krótszy, a nie dłuższy. - Konsystentnie obsługuj anulowania i timeouty: zwróć czysty błąd, zatrzymaj pracę i unikaj retry wewnątrz anulowanego żądania.
Potem zweryfikuj zachowanie pod obciążeniem. Timeout, który się uruchamia, ale nie zwalnia zasobów wystarczająco szybko, nadal szkodzi niezawodności.
Dashboardy powinny pokazywać timeouty jawnie, nie ukrywać ich w średnich. Śledź kilka sygnałów odpowiadających na pytanie „czy deadline’y są rzeczywiście egzekwowane?”: timeouty żądań i DB (oddzielnie), percentyle latencji (p95, p99), statystyki puli DB (połączenia w użyciu, wait count, wait duration) i rozbicie przyczyn błędów (context deadline exceeded vs inne błędy).
Jeśli budujesz wewnętrzne narzędzia na platformie takiej jak AppMaster, ta sama lista kontrolna ma zastosowanie do wszystkich usług Go, które z nią łączysz: zdefiniuj deadliny na granicy, propaguj je i potwierdź w metrykach, że zawieszone żądania stają się szybkim niepowodzeniem zamiast powolnych backlogów.
Przykładowy scenariusz i kolejne kroki
Częste miejsce, gdzie to się opłaca, to endpoint wyszukiwania. Wyobraź sobie GET /search?q=printer, który spowalnia, gdy baza jest obciążona dużym zapytaniem raportowym. Bez deadline’u każde przychodzące żądanie może czekać na długie zapytanie SQL. Pod obciążeniem te utkniete żądania się kumulują, blokują workerów i połączenia i całe API wydaje się zamarłe.
Z jasnym deadline’em w handlerze HTTP i tym samym ctx przekazanym do repo, system przestaje czekać, gdy budżet się skończy. Gdy deadline nadejdzie, driver bazy anulje zapytanie (jeśli to wspierane), handler zwróci odpowiedź, i serwer będzie mógł obsługiwać nowe żądania zamiast czekać w nieskończoność.
Dla użytkownika zachowanie jest lepsze nawet gdy coś pójdzie nie tak. Zamiast kręcić się przez 30–120 sekund i kończyć chaotycznie, klient otrzymuje szybki, przewidywalny błąd (często 504 lub 503 z krótkim komunikatem jak "request timed out"). Co ważniejsze, system szybciej wraca do pracy, bo nowe żądania nie blokują się za starymi.
Kolejne kroki, żeby to ujednolicić w całym serwisie i zespołach:
- Wybierz standardowe timeouty według typu endpointu (wyszukiwanie vs zapisy vs eksporty).
- Wymagaj
QueryContextiExecContextw code review. - Eksponuj błąd timeoutu na brzegu (czytelny status code, prosty komunikat).
- Dodaj metryki timeoutów i anulowań, żeby zauważyć regresje wcześnie.
- Napisz helper, który opakuje tworzenie kontekstu i logowanie, żeby każdy handler zachowywał się tak samo.
Jeśli budujesz serwisy i narzędzia wewnętrzne z AppMaster, możesz stosować te zasady spójnie w wygenerowanych backendach Go, integracjach API i dashboardach w jednym miejscu. AppMaster jest dostępny pod adresem appmaster.io (no-code, z generacją rzeczywistego kodu Go), więc może być praktycznym wyborem, gdy chcesz spójnego obsługiwania żądań i obserwowalności bez ręcznego budowania każdego narzędzia administracyjnego.
FAQ
Żądanie jest „zawieszone”, gdy czeka na coś, co nie zwraca odpowiedzi — wolne zapytanie SQL, zablokowane połączenie z puli, problemy z DNS lub serwis zewnętrzny, który nigdy nie odpowiada. Pod obciążeniem takie żądania gromadzą się, zajmują workerów i połączenia, i mogą zamienić niewielkie spowolnienie w szeroki outage.
Ustaw ogólny deadline na brzegu HTTP i przekaż ten sam ctx do każdej warstwy, która może blokować. Ten wspólny deadline zapobiega temu, by kilka wolnych operacji trzymało zasoby na tyle długo, żeby spowodować lawinę timeoutów.
Używaj ctx, cancel := context.WithTimeout(r.Context(), d) i zawsze defer cancel() w handlerze (lub middleware). Wywołanie cancel zwalnia timery i pomaga szybko zakończyć oczekiwanie, gdy żądanie skończy się wcześniej.
Nie zastępuj kontekstu context.Background() ani context.TODO() w kodzie obsługującym żądania — to łamie mechanizm anulowania i deadline’ów. Jeśli porzucisz kontekst żądania, prace w dół stosu, jak SQL czy outbound HTTP, mogą działać dalej nawet po zamknięciu klienta.
Traktuj context.DeadlineExceeded i context.Canceled jako normalne wyniki sterujące i propaguj je w górę bez zmiany. Na brzegu mapuj je na jasne odpowiedzi (często 504 dla timeoutów), żeby klienci nie wykonywali ślepych retry na pozornie losowych 500.
Używaj metod z obsługą kontekstu wszędzie: QueryContext, QueryRowContext, ExecContext i PrepareContext. Jeśli wywołasz Query() lub Exec() bez kontekstu, handler może się timeoutować, ale wywołanie DB może dalej blokować goroutine i trzymać połączenie.
Wiele driverów potrafi przerwać zapytanie po anulowaniu kontekstu, ale warto to zweryfikować w swoim stosie przez uruchomienie celowo powolnego zapytania i potwierdzenie szybkiego zwrotu po przekroczeniu deadline’u. Dodatkowo sensowne jest ustawienie po stronie DB limitu dla pojedynczego zapytania jako backstopu.
Buduj zapytanie z użyciem http.NewRequestWithContext(ctx, ...), aby ten sam deadline i anulowanie przepłynęły dalej. Dodatkowo skonfiguruj http.Client i transport: ustaw http.Client.Timeout i timeouty transportu (dial, TLS handshake, response header), bo kontekst nie ochroni przed każdym rodzajem zacięcia.
Unikaj tworzenia nowych kontekstów, które wydłużają budżet czasu w niższych warstwach; konteksty potomne powinny być krótsze, nie dłuższe. Jeśli w żądaniu pozostało niewiele czasu, pomiń opcjonalne wywołania zewnętrzne, zwróć częściowe dane lub zakończ szybko z jasnym błędem.
Monitoruj oddzielnie timeouty i anulowania według endpointu i zależności, wraz z percentylami latencji i liczbą żądań w locie. W trace’ach śledź ten sam kontekst od handlera przez wywołania outbound i QueryContext, aby zobaczyć, czy czas był spędzony na oczekiwaniu na połączenie DB, wykonaniu zapytania czy blokadzie na inną usługę.


