6 min

Timeouty context w Go: praktyczny przepis na szybkie API

Timeouty kontekstu w Go zapobiegają gromadzeniu się wolnych wywołań do bazy i zewnętrznych serwisów. Naucz się propagacji deadline, anulowania i bezpiecznych domyślnych ustawień.

Timeouty context w Go: praktyczny przepis na szybkie API

Dlaczego wolne żądania mogą „stopić” API

Pojedyncze wolne żądanie rzadko jest „po prostu wolne”. Podczas oczekiwania utrzymuje goroutine, zajmuje pamięć na bufory i obiekty odpowiedzi, a często blokuje też połączenie do bazy danych lub miejsce w puli. Gdy wystarczająco dużo wolnych żądań się nawarstwi, twoje API przestaje robić użyteczną pracę, bo ograniczone zasoby siedzą i czekają.

Zazwyczaj odczujesz to w trzech miejscach. Goroutine się kumulują i rośnie narzut planowania, więc opóźnienia pogarszają się dla wszystkich. Pule połączeń do bazy danych kończą wolne sloty, więc nawet szybkie zapytania zaczynają stać w kolejce za wolnymi. Pamięć rośnie z powodu danych w locie i częściowo zbudowanych odpowiedzi, co zwiększa pracę GC.

Dodanie więcej serwerów często tego nie naprawia. Jeśli każda instancja trafia na ten sam wąski gardło (mała pula DB, jeden wolny upstream, współdzielone limity), po prostu przesuwasz kolejkę i płacisz więcej, podczas gdy błędy nadal rosną.

Wyobraź sobie handler, który rozgłasza pracę: ładuje użytkownika z PostgreSQL, wywołuje serwis płatności, a potem serwis rekomendacji. Jeśli wywołanie rekomendacji zawiesi się i nic go nie anuluje, żądanie nigdy się nie kończy. Połączenie do DB może zostać zwrócone, ale goroutine i zasoby klienta HTTP pozostają związane. Pomnóż to przez setki żądań i dostajesz powolne załamanie.

Cel jest prosty: ustal czytelny limit czasu, zatrzymaj pracę, gdy czas minie, zwolnij zasoby i zwróć przewidywalny błąd. Timeouty context w Go dają każdemu krokowi deadline, dzięki czemu praca przestaje, gdy użytkownik już nie czeka.

Timeouty context — prosto

context.Context to mały obiekt, który przekazujesz w dół łańcucha wywołań, żeby każda warstwa zgadzała się co do jednej rzeczy: kiedy to żądanie musi się zatrzymać. Timeouty to powszechny sposób, by zapobiec przywiązaniu serwera do jednej wolnej zależności.

Kontekst może nieść trzy rodzaje informacji: deadline (kiedy praca musi się zatrzymać), sygnał anulowania (ktoś postanowił przerwać wcześniej) oraz kilka wartości związanych z żądaniem (używaj oszczędnie i nigdy nie trzymaj tam dużych danych).

Anulowanie nie jest magią. Kontekst udostępnia kanał Done(). Kiedy się zamyka, żądanie zostało anulowane lub skończył się czas. Kod, który respektuje kontekst, sprawdza Done() (często z select) i wraca wcześniej. Możesz też sprawdzić ctx.Err() by dowiedzieć się, dlaczego się zakończył — zwykle context.Canceled lub context.DeadlineExceeded.

Użyj context.WithTimeout żeby „zatrzymać po X sekundach”. Użyj context.WithDeadline gdy znasz już dokładny czas odcięcia. Użyj context.WithCancel gdy jakiś warunek rodzica powinien zatrzymać pracę (klient się rozłączył, użytkownik opuścił stronę, albo masz już odpowiedź).

Gdy kontekst jest anulowany, poprawne zachowanie jest nudne, ale ważne: przestań robić pracę, przestań czekać na wolne I/O i zwróć jasny błąd. Jeśli handler czeka na zapytanie do bazy danych i kontekst się kończy, wróć szybko i pozwól, by zapytanie zostało przerwane, jeśli sterownik/DB to obsługują.

Ustal limity czasu na granicy API

Najbezpieczniejsze miejsce, by zatrzymywać wolne żądania, to granica, gdzie ruch wchodzi do usługi. Jeśli żądanie ma czasowi out, chcesz, żeby działo się to przewidywalnie i wcześnie — nie po tym, jak zdążyło zająć goroutine, połączenia do DB i pamięć.

Zacznij od krawędzi (load balancer, API gateway, reverse proxy) i ustaw twardy limit, jak długo każde żądanie może żyć. To chroni twój serwer Go nawet jeśli handler zapomni ustawić timeout.

W samym serwerze Go ustaw timeouty HTTP, żeby serwer nie czekał w nieskończoność na wolnego klienta lub zatrzymaną odpowiedź. Minimum to timeouty na odczyt nagłówków, odczyt całego ciała żądania, zapis odpowiedzi i utrzymywanie połączeń idle.

Wybierz domyślny budżet żądania dopasowany do produktu. Dla wielu API 1–3 sekundy to rozsądny punkt wyjścia dla typowych żądań, z wyższym limitem dla znanych wolnych operacji (np. eksporty). Dokładna liczba ma mniejsze znaczenie niż konsekwencja, pomiar i jasna zasada wyjątków.

Odpowiedzi strumieniowe wymagają dodatkowej uwagi. Łatwo przez pomyłkę stworzyć nieskończony strumień, gdzie serwer trzyma połączenie otwarte i zapisuje maleńkie porcje w nieskończoność, lub czeka w nieskończoność przed pierwszym bajtem. Zdecyduj zawczasu, czy endpoint jest naprawdę strumieniem. Jeśli nie jest, wymusz limit całkowitego czasu i limit czasu do pierwszego bajtu.

Gdy granica ma jasny deadline, łatwiej jest propagować go przez całe żądanie.

Krok po kroku: dodanie timeoutu do handlera HTTP

Najprostsze miejsce na start to handler HTTP. To punkt wejścia jednego żądania do systemu, więc naturalne miejsce na ustawienie twardego limitu.

1) Wyprowadź kontekst z limitem czasu

Utwórz nowy kontekst z deadline i pamiętaj, aby go anulować. Przekaż ten kontekst do wszystkiego, co może się blokować: pracy w bazie danych, wywołań HTTP lub wolnych obliczeń.

func (s *Server) GetUser(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
	defer cancel()

	userID := r.URL.Query().Get("id")
	if userID == "" {
		http.Error(w, "missing id", http.StatusBadRequest)
		return
	}

	user, err := s.loadUser(ctx, userID)
	if err != nil {
		writeError(w, ctx, err)
		return
	}

	writeJSON(w, http.StatusOK, user)
}

2) Przekaż ctx do każdego blokującego wywołania

Dobra zasada: jeśli funkcja może czekać na I/O, powinna akceptować context.Context. Utrzymuj handlery czytelnymi, przenosząc szczegóły do małych helperów jak loadUser.

func (s *Server) loadUser(ctx context.Context, id string) (User, error) {
	return s.repo.GetUser(ctx, id) // repo powinno używać QueryRowContext/ExecContext
}

3) Wracaj wcześniej, gdy kontekst się skończy

Jeśli deadline minie (albo klient się rozłączy), zakończ pracę i zwróć przyjazną odpowiedź. Częsta mapa to context.DeadlineExceeded -> 504 Gateway Timeout, a context.Canceled -> „klient odszedł” (często bez ciała odpowiedzi).

func writeError(w http.ResponseWriter, ctx context.Context, err error) {
	if errors.Is(err, context.DeadlineExceeded) {
		http.Error(w, "request timed out", http.StatusGatewayTimeout)
		return
	}
	if errors.Is(err, context.Canceled) {
		// Klient się rozłączył. Unikaj dalszej pracy.
		return
	}
	http.Error(w, "internal error", http.StatusInternalServerError)
}

Ten wzorzec zapobiega nawarstwianiu się żądań. Gdy timer wygaśnie, każda funkcja respektująca context dalej w łańcuchu otrzyma ten sam sygnał stop i może szybko zakończyć pracę.

Propagacja deadline do wywołań PostgreSQL

Gdy handler ma kontekst z deadline, najważniejsza zasada jest prosta: używaj tego samego ctx aż do wywołania bazy danych. Dzięki temu timeouty faktycznie zatrzymują pracę, a nie tylko przestają czekać twój handler.

W database/sql preferuj metody świadome kontekstu:

func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    row := s.db.QueryRowContext(ctx,
        "SELECT id, email FROM users WHERE id = $1",
        r.URL.Query().Get("id"),
    )

    var id int64
    var email string
    if err := row.Scan(&id, &email); err != nil {
        // handle below
    }
}

Dobierz timeout DB do budżetu żądania

Jeśli budżet handlera to 2 sekundy, baza powinna dostać tylko część tego. Zostaw czas na kodowanie JSON, inne zależności i obsługę błędów. Prosty punkt startowy to przyznać Postgresowi 30–60% całego budżetu. Przy 2 sekundach to może być 800ms–1.2s.

Co się dzieje przy anulowaniu zapytania

Gdy kontekst zostanie anulowany, sterownik poprosi Postgresa o zatrzymanie zapytania. Zwykle połączenie wraca do puli i można je ponownie użyć. Jeśli anulowanie nastąpi podczas złego momentu sieciowego, sterownik może odrzucić to połączenie i później otworzyć nowe. W każdym wypadku unikasz goroutine czekającej w nieskończoność.

Sprawdzając błędy, traktuj timeouty inaczej niż zwykłe błędy DB. Jeśli errors.Is(err, context.DeadlineExceeded), zabrakło czasu i powinieneś zwrócić timeout. Jeśli errors.Is(err, context.Canceled), klient odszedł i powinieneś spokojnie przerwać pracę. Inne błędy to normalne problemy zapytań (błędny SQL, brak wiersza, uprawnienia).

Propagacja deadline do zewnętrznych wywołań HTTP

Standardize request timeouts
Turn your timeout budget rules into consistent handler patterns across routes.

Jeśli handler ma deadline, twoje wyjściowe wywołania HTTP też powinny go respektować. W przeciwnym razie klient się podda, ale twój serwer będzie nadal czekał na wolny upstream i będzie blokował goroutine, gniazda i pamięć.

Buduj wychodzące requesty z nadrzędnym kontekstem, żeby anulowanie przeszło automatycznie:

func fetchUser(ctx context.Context, url string) ([]byte, error) {
	// Add a small per-call cap, but never exceed the parent deadline.
	ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close() // always close, even on non-200

	return io.ReadAll(resp.Body)
}

Ten per-call timeout to siatka bezpieczeństwa. Nadrzędny deadline nadal jest przełożonym. Jedna zegarowa granica dla całego żądania, plus mniejsze limity dla ryzykownych kroków.

Skonfiguruj też timeouty na poziomie transportu. Kontekst anuluje request, ale timeouty transportu chronią przed wolnymi handshake'ami i serwerami, które nigdy nie wysyłają nagłówków.

Jeden szczegół, który potrafi zespołom spłatać figla: ciała odpowiedzi muszą być zamykane na każdej ścieżce. Jeśli wracasz wcześniej (sprawdzenie statusu, błąd dekodowania JSON, timeout kontekstu), i tak zamknij body. Wycieki body mogą cicho wyczerpać połączenia w puli i zamienić się w „losowe” skoki latencji.

Konkretna scena: twoje API wywołuje dostawcę płatności. Klient time'outuje po 2 sekundach, ale upstream wisi przez 30 sekund. Bez anulowania requestu i timeoutów transportu płacisz za te 30 sekund czekania dla każdego porzuconego żądania.

Budżetowanie timeoutów przez całe żądanie

Jedno żądanie zwykle dotyka więcej niż jednej powolnej rzeczy: praca handlera, zapytanie do bazy i jedno lub więcej wywołań zewnętrznych. Jeśli każdemu krokowi dasz hojne timeouty, całkowity czas cicho rośnie, aż użytkownicy to poczują i serwer zacznie się zapychać.

Budżetowanie jest najprostszą naprawą. Ustaw jeden nadrzędny deadline dla całego żądania, a następnie daj każdej zależności mniejszy kawałek. Deadliny potomków powinny być wcześniejsze niż rodzica, żebyś mógł szybko niepowieść i wciąż mieć czas na zwrócenie czystego błędu.

Zasady, które sprawdzają się w realnych usługach:

  • Najpierw wybierz całkowity budżet (np. 2 sekundy dla endpointu skierowanego do użytkownika).
  • Zarezerwuj mały bufor na narzut handlera i formatowanie odpowiedzi (np. 100–200ms).
  • Podziel pozostały czas między zależności.
  • Jeśli masz wiele wywołań zewnętrznych, ogranicz każde z nich zamiast pozwalać jednemu zająć cały budżet.
  • Jeśli w nadrzędnym ctx zostało tylko 120ms, nie zaczynaj wywołania, które normalnie potrzebuje 300ms.

Unikaj nakładania timeoutów, które ze sobą walczą. Jeśli handler ma deadline 2s, a klient HTTP ma timeout 10s, jesteś bezpieczny choć to mylące. Jeśli jest odwrotnie, klient może odciąć pracę wcześniej z innych powodów.

Dla pracy w tle (logi audytu, metryki, emaile) nie używaj kontekstu żądania. Użyj osobnego kontekstu z własnym krótkim timeoutem, by anulowanie przez klienta nie zabijało istotnego sprzątania.

Typowe błędy, które powodują wieszanie się żądań

Build context-aware Go APIs
Generate Go handlers that pass context deadlines into Postgres and outbound HTTP calls.

Większość bugów związanych z timeoutami nie jest w handlerze. Pojawiają się jedną lub dwie warstwy niżej, gdzie deadline cicho ginie. Jeśli ustawisz timeout na krawędzi, ale zignorujesz go gdzieś pośrodku, nadal możesz mieć goroutine, zapytania DB lub wywołania HTTP, które będą działać dalej po tym, jak klient odszedł.

Najczęściej pojawiające się wzorce są proste:

  • Porzucenie kontekstu żądania i wywoływanie warstw niższych z context.Background() (albo TODO). To odłącza pracę od anulowania klienta i deadline handlera.
  • sleep, retry lub pętle bez sprawdzania ctx.Done(). Żądanie jest anulowane, ale twój kod dalej czeka.
  • Owijanie każdego helpera w jego własny context.WithTimeout. Kończy się wieloma timerami i mylącymi deadline'ami.
  • Zapomnienie o dołączeniu ctx do blokujących wywołań (DB, HTTP, publikacja wiadomości). Timeout handlera nic nie da, jeśli zależność go ignoruje.
  • Traktowanie timeoutów jak zwykłych błędów i zwracanie 500. Klienci potrzebują jasnego sygnału, że zabrakło czasu.

Klasyczna porażka: dodajesz 2s timeout w handlerze, a repo używa context.Background() dla zapytania DB. Pod obciążeniem wolne zapytanie nadal działa nawet po tym, jak klient się poddał, i kolejka rośnie.

Napraw podstawy: przekaż ctx jako pierwszy argument przez stos wywołań. W długiej pracy dodaj szybkie sprawdzenia jak select { case <-ctx.Done(): return ctx.Err() default: }. Mapuj context.DeadlineExceeded na odpowiedź timeout (często 504), a context.Canceled na styl odpowiedzi klient-odszedł (często 408 lub 499 zależnie od konwencji).

Testowanie i obserwowanie timeoutów w praktyce

Timeouty pomagają tylko wtedy, gdy możesz je zobaczyć i potwierdzić, że system się ładnie odzyskuje. Gdy coś jest wolne, żądanie powinno się zatrzymać, zasoby powinny zostać zwolnione, a API powinno pozostać responsywne.

Loguj to, co się liczy (i trzymaj to spójnym)

Dla każdego żądania loguj ten sam mały zestaw pól, aby móc porównywać normalne żądania vs timeouty. Dołącz deadline kontekstu (jeśli istnieje) i co zakończyło pracę.

Przydatne pola to deadline (albo "none"), całkowity czas wykonania, powód anulowania (timeout vs client canceled), krótka etykieta operacji ("db.query users", "http.call billing") i request ID.

Minimalny wzorzec wygląda tak:

start := time.Now()
deadline, hasDeadline := ctx.Deadline()
err := doWork(ctx)
log.Printf("op=%s hasDeadline=%t deadline=%s elapsed=%s err=%v",
  "getUser", hasDeadline, deadline.Format(time.RFC3339Nano), time.Since(start), err)

Metryki, które pokazują timeouty zanim użytkownicy zaczną narzekać

Logi pomagają debugować pojedyncze żądanie. Metryki pokazują trendy.

Śledź kilka sygnałów, które zwykle rosną wcześnie, gdy timeouty są nieprawidłowe: liczba timeoutów po trasie i zależności, ilość żądań w locie (powinna się ustabilizować pod obciążeniem), czas oczekiwania w puli DB oraz percentyle latencji (p95/p99) rozdzielone na sukcesy vs timeouty.

Odwzoruj timeouty lokalnie (żeby ufać naprawie)

Spraw, by wolność była przewidywalna. Dodaj debugowy delay do jednego handlera, spowolnij zapytanie DB celowo, lub opakuj zewnętrzne wywołanie testowym serwerem, który śpi. Potem zweryfikuj dwie rzeczy: widzisz błąd timeout i praca zatrzymuje się szybko po anulowaniu.

Mały test obciążeniowy też się przyda. Uruchom 20–50 równoczesnych żądań przez 30–60 sekund z jedną wymuszoną wolną zależnością. Liczba goroutine i żądań w locie powinna wzrosnąć, a potem się ustabilizować. Jeśli ciągle rośnie, coś ignoruje anulowanie kontekstu.

Szybka lista kontrolna przed wysyłką

Timeouty pomagają tylko, jeśli są stosowane wszędzie, gdzie żądanie może czekać. Zrób jednorazowe przeglądnięcie kodu i upewnij się, że te same zasady są przestrzegane w każdym handlerze.

  • Każde przychodzące żądanie ma czytelny budżet czasu na granicy (router, middleware, handler). Brak nieskończonych handlerów.
  • Każde zapytanie do bazy otrzymuje kontekst żądania (lub kontekst potomny) i sprawdza błędy pod kątem context.DeadlineExceeded i context.Canceled.
  • Każde wyjściowe wywołanie HTTP używa http.NewRequestWithContext (lub req = req.WithContext(ctx)) i klient ma timeouty transportu (dial, TLS, response header). Unikaj polegania na http.DefaultClient w ścieżkach produkcyjnych.
  • Błędy są mapowane konsekwentnie: timeouty stają się taką samą odpowiedzią API za każdym razem (często 504), anulowania klienta mapowane są czytelnie (często 499 lub 408 zależnie od konwencji), a wewnętrzne timeouty nie wyciekają jako surowe błędy sterownika.
  • Logi i metryki czynią timeouty oczywistymi: nazwa handlera, upłynięty czas i która zależność przekroczyła limit.

Krótki "slow dependency" drill przed wydaniem jest tego wart. Dodaj sztuczne 2s opóźnienie do jednego zapytania SQL i potwierdź trzy rzeczy: handler zwraca na czas, zapytanie DB naprawdę się zatrzymiało (nie tylko handler), i logi jasno mówią, że to był DB timeout.

Realistyczny przykład: jedno żądanie, trzy wolne zależności

Keep full control of code
Export the source and apply your own middleware for deadlines, logs, and error mapping.

Wyobraź sobie endpoint GET /v1/account/summary. Jedna akcja użytkownika uruchamia trzy rzeczy: zapytanie PostgreSQL (konto + ostatnia aktywność) i dwa wywołania HTTP (np. sprawdzenie statusu płatności i wzbogacenie profilu).

Daj całemu żądaniu twardy budżet 2 sekund. Bez budżetu jedna wolna zależność może trzymać goroutine, połączenia DB i pamięć, aż twoje API zacznie timeoutować wszędzie.

Prosty podział to 800ms na DB, 600ms na wywołanie zewnętrzne A i 600ms na wywołanie zewnętrzne B.

Gdy znasz deadline, przekaż go w dół. Każda zależność dostaje swój mniejszy timeout, ale nadal dziedziczy anulowanie od rodzica.

func AccountSummary(w http.ResponseWriter, r *http.Request) {
  ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
  defer cancel()

  dbCtx, dbCancel := context.WithTimeout(ctx, 800*time.Millisecond)
  defer dbCancel()

  aCtx, aCancel := context.WithTimeout(ctx, 600*time.Millisecond)
  defer aCancel()

  bCtx, bCancel := context.WithTimeout(ctx, 600*time.Millisecond)
  defer bCancel()

  // Use dbCtx for QueryContext, aCtx/bCtx for outbound HTTP requests.
}

Jeśli zewnętrzne wywołanie B spowalnia i zajmuje 2.5 sekundy, twój handler powinien przestać czekać po 600ms, anulować pracę w locie i zwrócić jasny timeout klientowi. Klient dostaje szybkie niepowodzenie zamiast zawieszonego spinnera.

W logach powinno być jasne, co wykorzystało budżet: DB skończył szybko, zewnętrzne A się powiodło, zewnętrzne B przekroczyło swój limit i zwróciło context deadline exceeded.

Następne kroki: ustandaryzuj timeouty w całym API

Gdy jeden endpoint realnie dobrze działa z timeoutami i anulowaniem, zamień to w powtarzalny wzorzec. Zastosuj end-to-end: deadline w handlerze, wywołania DB i outbound HTTP. Potem kopiuj tę samą strukturę do kolejnych endpointów.

Pójdzie szybciej, jeśli scentralizujesz nudne części: helper do timeoutów na granicy, wrappery zapewniające przekazanie ctx do DB i HTTP oraz jedna spójna mapa błędów i format logów.

Jeśli chcesz szybko prototypować ten wzorzec, Koder.ai (koder.ai) może wygenerować handlery Go i wywołania serwisów na podstawie promptu w czacie, a następnie wyeksportować kod źródłowy, żebyś mógł dodać swoje middleware do deadline'ów, logów i mapowania błędów. Cel to konsekwencja: wolne wywołania zatrzymują się wcześnie, błędy wyglądają tak samo i debugowanie nie zależy od tego, kto napisał endpoint.

Często zadawane pytania

Why can a few slow requests bring down an otherwise healthy API?

Wolne żądanie zajmuje ograniczone zasoby podczas oczekiwania: goroutine, pamięć na bufory i obiekty odpowiedzi oraz często połączenie z bazą danych lub połączenie HTTP. Gdy wiele takich żądań czeka jednocześnie, tworzą się kolejki, opóźnienia rosną dla wszystkich żądań, a usługa może zacząć zawodzić mimo że każde żądanie z osobna w końcu by się zakończyło.

What’s the simplest way to prevent slow requests from piling up in Go?

Ustaw jasny deadline na granicy żądania (proxy/gateway i w serwerze Go), stwórz w handlerze kontekst z limitem czasu i przekaż ten ctx do każdego blokującego wywołania (baza danych i outbound HTTP). Gdy deadline minie, szybko zwróć spójną odpowiedź timeout i przerwij pracę, którą można anulować.

When should I use WithTimeout vs WithDeadline vs WithCancel?

Użyj context.WithTimeout(parent, d) gdy chcesz „zatrzymaj po upływie tej długości czasu” — to najczęstszy przypadek w handlerach. Użyj context.WithDeadline(parent, t) gdy masz już konkretny czas graniczny do zachowania. Użyj context.WithCancel(parent) gdy jakiś warunek wewnętrzny powinien wcześniej przerwać pracę, np. gdy „mamy już odpowiedź” lub „klient się rozłączył”.

Why do I need to call cancel() if the timeout will happen anyway?

Zawsze wywołuj funkcję cancel (zwykle defer cancel() bezpośrednio po utworzeniu pochodnego kontekstu). Anulowanie zwalnia timer i daje jasny sygnał zatrzymania pracy dzieciom, szczególnie w ścieżkach, które wracają wcześniej niż upłynie deadline.

How do I make sure my deadline actually reaches lower layers?

Stwórz kontekst w handlerze i przekaż go dalej jako pierwszy argument do funkcji, które mogą się blokować. Szybkim sposobem na sprawdzenie problemów jest wyszukanie context.Background() lub context.TODO() w ścieżkach obsługi żądań — one często przerywają propagację anulowania i deadline.

How do I apply timeouts to PostgreSQL queries properly?

Używaj metod QueryContext, QueryRowContext oraz ExecContext (albo ich odpowiedników w twoim driverze). Gdy kontekst się zakończy, driver poprosi Postgresa o anulowanie zapytania, więc nie będziesz tracić czasu i połączeń po stronie serwera.

How do I stop outbound HTTP calls from hanging after the client gave up?

Dołącz nadrzędny kontekst żądania do wyjściowego requestu za pomocą http.NewRequestWithContext(ctx, ...) i skonfiguruj również timeouty transportu klienta, aby chronić się przy łączeniu, TLS i oczekiwaniu na nagłówki. Nawet przy błędach czy nie-200 odpowiedziach zawsze zamykaj resp.Body, żeby połączenia wracały do puli.

How should I “budget” time across DB work and multiple external calls?

Najpierw wybierz całkowity budżet żądania, potem podziel go na mniejsze kawałki dla zależności, zostawiając mały margines na overhead handlera i formatowanie odpowiedzi. Jeśli nadrzędny kontekst ma mało czasu, nie zaczynaj kosztownej pracy, która nie zdąży przed deadline.

What status code should I return on context timeouts and cancellations?

Często mapuje się context.DeadlineExceeded na 504 Gateway Timeout z krótkim komunikatem typu „request timed out”. context.Canceled zwykle oznacza, że klient się rozłączył — najlepszą akcją jest zakończyć pracę i zwykle nie wysyłać ciała odpowiedzi, żeby nie marnować zasobów.

What are the most common mistakes that make handlers ignore timeouts?

Najczęstsze błędy to: porzucenie kontekstu żądania przez użycie context.Background(), uruchamianie retry/sleep/pętli bez sprawdzania ctx.Done(), zapominanie o dołączeniu ctx do blokujących wywołań, oraz niespójne nakładanie wielu timeoutów w różnych warstwach, co utrudnia zrozumienie, który timeout wygrał.

Related posts