Go-Context-Timeouts für APIs: von HTTP-Handlern bis zu SQL-Aufrufen
Go-Context-Timeouts für APIs helfen, Deadlines von HTTP-Handlern bis zu SQL-Aufrufen durchzureichen, blockierte Anfragen zu verhindern und Dienste unter Last stabil zu halten.

Warum Anfragen stecken bleiben (und warum das unter Last schadet)
Eine Anfrage bleibt „stecken“, wenn sie auf etwas wartet, das nicht zurückkommt: eine langsame Datenbankabfrage, eine blockierte Verbindung aus dem Pool, ein DNS-Problem oder ein Upstream-Dienst, der den Aufruf annimmt, aber nie antwortet.
Das Symptom wirkt einfach: manche Anfragen dauern ewig, und immer mehr warten hinter ihnen. Häufig sehen Sie steigenden Speicherverbrauch, eine wachsende Anzahl von Goroutinen und eine Liste offener Verbindungen, die sich nicht zu leeren scheint.
Unter Last schadet das doppelt. Solche Anfragen binden Worker und halten knappe Ressourcen wie Datenbankverbindungen und Locks. Das macht normalerweise schnelle Anfragen langsam, was zu mehr Überlappung führt und noch mehr Wartezeiten erzeugt.
Retries und Traffic-Spitzen verschlimmern diese Spirale. Ein Client läuft in ein Timeout und versucht es erneut, während die ursprüngliche Anfrage noch läuft — jetzt bezahlen Sie für zwei Anfragen. Das multipliziert sich bei vielen Clients während einer kurzen Verlangsamung und kann die Datenbank überlasten oder Verbindungsgrenzen erreichen, selbst wenn der durchschnittliche Traffic in Ordnung ist.
Ein Timeout ist einfach ein Versprechen: „wir warten nicht länger als X“. Es hilft, schnell zu fehlschlagen und Ressourcen freizugeben, macht die Arbeit aber nicht schneller.
Es garantiert auch nicht, dass die Arbeit sofort stoppt. Die Datenbank könnte weiter ausführen, ein Upstream-Dienst die Abbruchsignale ignorieren oder Ihr eigener Code nicht sicher mit Cancelation umgehen.
Was ein Timeout garantiert, ist, dass Ihr Handler aufhören kann zu warten, einen klaren Fehler zurückgibt und das Freigebundene abgibt. Dieses begrenzte Warten verhindert, dass ein paar langsame Aufrufe zu einem vollständigen Ausfall führen.
Das Ziel bei Go-Context-Timeouts ist eine gemeinsame Deadline vom Rand bis zur tiefsten Operation. Setzen Sie sie einmal an der HTTP-Grenze, geben Sie denselben Kontext durch Ihre Service-Schichten weiter und nutzen Sie ihn in database/sql-Aufrufen, damit auch die Datenbank weiß, wann sie aufhören soll zu warten.
Context in Go in plain terms
Ein context.Context ist ein kleines Objekt, das Sie durch Ihren Code reichen, um zu beschreiben, was gerade passiert. Es beantwortet Fragen wie: „Gilt diese Anfrage noch?“, „Wann sollen wir aufgeben?“ und „Welche anfragebezogenen Werte sollen mit dieser Arbeit mitlaufen?"
Der große Gewinn ist, dass eine Entscheidung am Rand Ihres Systems (Ihr HTTP-Handler) jeden nachgelagerten Schritt schützen kann, solange Sie denselben Kontext weiterreichen.
Was Context trägt
Context ist kein Ort für Business-Daten. Er ist für Kontrollsignale und eine kleine Menge an anfragebezogenem Scope: Cancelation, eine Deadline/Timeout und kleine Metadaten wie eine Request-ID für Logs.
Timeout vs. Cancelation ist einfach: Ein Timeout ist ein Grund für Cancelation. Wenn Sie ein 2-Sekunden-Timeout setzen, wird der Kontext nach 2 Sekunden abgebrochen. Aber ein Kontext kann auch früher abgebrochen werden, wenn der Benutzer den Tab schließt, der Load Balancer die Verbindung trennt oder Ihr Code entscheidet, die Anfrage zu beenden.
Context fließt durch Funktionsaufrufe als expliziter Parameter, üblicherweise der erste: func DoThing(ctx context.Context, ...). Genau darum geht es. So vergisst man ihn schwer, weil er an jeder Aufrufstelle sichtbar ist.
Wenn die Deadline abläuft, sollten alle Komponenten, die den Kontext beobachten, schnell stoppen. Eine Datenbank-Abfrage mit QueryContext sollte zum Beispiel früh mit einem Fehler wie context deadline exceeded zurückkehren, und Ihr Handler kann mit einem Timeout antworten, anstatt ewig zu hängen, bis der Server keine Worker mehr hat.
Ein gutes mentales Modell: eine Anfrage, ein Kontext, überall weitergereicht. Wenn die Anfrage stirbt, sollte auch die Arbeit sterben.
Eine klare Deadline an der HTTP-Grenze setzen
Wenn End-to-End-Timeouts funktionieren sollen, entscheiden Sie, wo die Uhr startet. Der sicherste Ort ist direkt am HTTP-Rand, damit jeder nachgelagerte Aufruf (Business-Logik, SQL, andere Dienste) dieselbe Deadline erbt.
Sie können diese Deadline an mehreren Stellen setzen. Serverweite Timeouts sind eine gute Basis und schützen vor langsamen Clients. Middleware ist nützlich für Konsistenz über Routen-Gruppen. Sie können sie auch im Handler setzen, wenn Sie etwas Explizites und Lokales wollen.
Für die meisten APIs sind per-Request-Timeouts in Middleware oder im Handler am einfachsten zu begründen. Halten Sie sie realistisch: Nutzer bevorzugen ein schnelles, klares Scheitern gegenüber einer Anfrage, die hängt. Viele Teams verwenden kürzere Budgets für Leseanfragen (z. B. 1–2s) und etwas längere für Schreiboperationen (z. B. 3–10s), abhängig von der Arbeit des Endpunkts.
Hier ein einfaches Handler-Muster:
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)
}
Zwei Regeln machen das effektiv:
- Rufen Sie immer
cancel()auf, damit Timer und Ressourcen schnell freigegeben werden. - Ersetzen Sie den Request-Kontext niemals durch
context.Background()odercontext.TODO()im Handler. Das bricht die Kette, und Ihre Datenbankaufrufe und ausgehenden Anfragen können weiterlaufen, selbst nachdem der Client verschwunden ist.
Kontext durch den Code propagieren
Sobald Sie eine Deadline an der HTTP-Grenze gesetzt haben, besteht die eigentliche Arbeit darin sicherzustellen, dass dieselbe Deadline jede Schicht erreicht, die blockieren kann. Die Idee ist eine Uhr, geteilt vom Handler, über die Service-Schicht bis hin zu allem, was Netzwerk oder Disk berührt.
Eine einfache Regel sorgt für Konsistenz: Jede Funktion, die warten könnte, sollte einen context.Context akzeptieren und er sollte der erste Parameter sein. Das macht es an den Aufrufstellen offensichtlich und zur Gewohnheit.
Ein praktisches Signaturmuster
Bevorzugen Sie Signaturen wie DoThing(ctx context.Context, ...) für Services und Repositories. Vermeiden Sie es, Context im Struct zu verstecken oder ihn mit context.Background() in tieferen Schichten neu zu erzeugen, denn das verwirft stillschweigend die Deadline des Aufrufers.
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 */)
}
Frühe Abbrüche sauber handhaben
Behandeln Sie ctx.Done() als normalen Kontrollpfad. Zwei Gewohnheiten helfen:
- Prüfen Sie
ctx.Err()bevor Sie teure Arbeit starten und nach langen Loops. - Geben Sie
ctx.Err()nach oben unverändert zurück, damit der Handler schnell antworten und keine Ressourcen weiter verschwenden kann.
Wenn jede Schicht denselben ctx weiterreicht, kann ein einzelnes Timeout Parsen, Business-Logik und Datenbank-Wartezeiten auf einmal abschneiden.
Deadlines auf database/sql-Abfragen anwenden
Sobald Ihr HTTP-Handler eine Deadline hat, stellen Sie sicher, dass auch Ihre Datenbankarbeit darauf hört. Mit database/sql bedeutet das, jedes Mal die kontextbewussten Methoden zu verwenden. Wenn Sie Query() oder Exec() ohne Kontext aufrufen, kann Ihre API weiter auf eine langsame Abfrage warten, obwohl der Client bereits aufgegeben hat.
Verwenden Sie konsequent: db.QueryContext, db.QueryRowContext, db.ExecContext und db.PrepareContext (und dann QueryContext/ExecContext auf dem zurückgegebenen 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
}
Zwei Dinge übersieht man leicht.
Erstens muss Ihr SQL-Treiber Kontext-Abbruch respektieren. Viele tun das, aber überprüfen Sie es in Ihrem Stack, indem Sie eine absichtlich langsame Abfrage testen und prüfen, ob sie beim Ablauf der Deadline schnell abbricht.
Zweitens sollten Sie eine Datenbank-seitige Timeout-Absicherung in Betracht ziehen. Zum Beispiel kann Postgres ein per-Statement-Timeout erzwingen. Das schützt die Datenbank, falls ein Fehler im Code dazu führt, dass irgendwo der Kontext vergessen wird.
Wenn eine Operation wegen Timeout stoppt, behandeln Sie das anders als einen normalen SQL-Fehler. Prüfen Sie errors.Is(err, context.DeadlineExceeded) und errors.Is(err, context.Canceled) und geben Sie eine klare Antwort (z. B. 504) statt "Datenbank ist kaputt". Wenn Sie Go-Backends generieren (z. B. mit AppMaster), macht die Trennung dieser Fehlerpfade auch Logs und Retries leichter verständlich.
Downstream-Aufrufe: HTTP-Clients, Caches und andere Dienste
Selbst wenn Ihr Handler und die SQL-Abfragen Context respektieren, kann eine Anfrage noch hängenbleiben, wenn ein nachgelagerter Aufruf ewig wartet. Unter Last können einige feststeckende Goroutinen auflaufen, Connection-Pools aufbrauchen und eine kleine Verlangsamung in einen vollständigen Ausfall verwandeln. Die Lösung ist konsistente Weitergabe plus eine harte Absicherung.
Ausgehendes HTTP
Beim Aufrufen einer anderen API bauen Sie die Anfrage mit demselben Kontext, damit Deadline und Cancelation automatisch durchfließen.
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { /* handle */ }
resp, err := httpClient.Do(req)
Verlassen Sie sich nicht nur auf den Kontext. Konfigurieren Sie auch den HTTP-Client und Transport, damit Sie geschützt sind, falls Code versehentlich einen Background-Kontext verwendet oder DNS/TLS/Idle-Verbindungen hängen bleiben. Setzen Sie http.Client.Timeout als obere Grenze für den ganzen Aufruf, konfigurieren Sie Transport-Timeouts (Dial, TLS-Handshake, Response Header) und verwenden Sie einen wiederverwendbaren Client statt jedes Mal einen neuen zu erstellen.
Caches und Queues
Caches, Message-Broker und RPC-Clients haben oft eigene Wartepunkte: Verbindungsgewinnung, Warten auf eine Antwort, Blockieren in einer vollen Queue oder Warten auf ein Lock. Stellen Sie sicher, dass diese Operationen ctx akzeptieren, und nutzen Sie wo möglich Bibliotheks-Timeouts.
Eine praktische Regel: Wenn die User-Anfrage noch 800 ms übrig hat, starten Sie keinen Downstream-Aufruf, der 2 Sekunden dauern könnte. Überspringen Sie ihn, degradieren Sie die Antwort oder liefern Sie partielle Daten.
Entscheiden Sie im Voraus, was ein Timeout für Ihre API bedeutet. Manchmal ist die richtige Antwort ein schnelles Fehler-Response. Manchmal sind es optionale Felder mit Teildaten. Manchmal sind es veraltete Cache-Daten, klar markiert.
Wenn Sie Go-Backends bauen (einschließlich generierter mit AppMaster), ist das der Unterschied zwischen „es gibt Timeouts“ und „Timeouts schützen konsistent das System“, wenn Traffic-Spitzen auftreten.
Schritt-für-Schritt: API refactoren für End-to-End-Timeouts
Refactoring für Timeouts kommt auf eine Gewohnheit an: geben Sie denselben context.Context vom HTTP-Rand bis in jeden Aufruf weiter, der blockieren könnte.
Praktisch gehen Sie Top-Down vor:
- Ändern Sie Handler und Kern-Service-Methoden so, dass sie
ctx context.Contextakzeptieren. - Aktualisieren Sie alle DB-Aufrufe auf
QueryContextoderExecContext. - Machen Sie dasselbe für externe Aufrufe (HTTP-Clients, Caches, Queues). Wenn eine Bibliothek kein
ctxakzeptiert, wickeln Sie sie oder ersetzen Sie sie. - Entscheiden Sie, wer Timeouts verwaltet. Eine übliche Regel: Der Handler setzt die Gesamt-Deadline; tiefere Schichten setzen nur kürzere Deadlines für spezifische Operationen, wenn nötig.
- Machen Sie Fehler am Rand vorhersehbar: Mappen Sie
context.DeadlineExceededundcontext.Canceledauf klare HTTP-Antworten.
Hier ist die gewünschte Form über die Schichten hinweg:
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...
}
Timeout-Werte sollten langweilig und konsistent sein. Wenn der Handler 2 Sekunden insgesamt hat, halten Sie DB-Abfragen unter 1 Sekunde, um Platz für JSON-Encoding und andere Arbeit zu lassen.
Um zu beweisen, dass es funktioniert, fügen Sie einen Test hinzu, der ein Timeout erzwingt. Ein einfacher Ansatz ist eine Fake-Repository-Methode, die blockiert, bis ctx.Done() und dann ctx.Err() zurückgibt. Ihr Test sollte prüfen, dass der Handler schnell einen 504 zurückgibt, nicht erst nach der Fake-Verzögerung.
Wenn Sie Go-Backends mit einem Generator bauen (z. B. AppMaster generiert Go-Services), gilt dieselbe Regel: ein Request-Context, durchgängig weitergereicht, mit klarer Verantwortlichkeit für die Deadline.
Observability: nachweisen, dass Timeouts wirken
Timeouts helfen nur, wenn Sie sehen können, dass sie greifen. Das Ziel ist einfach: jede Anfrage hat eine Deadline, und wenn sie fehlschlägt, sehen Sie, wo die Zeit hinging.
Beginnen Sie mit Logs, die sicher und nützlich sind. Statt komplette Request-Bodies zu dumpen, loggen Sie genug, um Zusammenhänge zu erkennen und langsame Pfade sichtbar zu machen: Request-ID (oder Trace-ID), ob eine Deadline gesetzt ist und wie viel Zeit an wichtigen Stellen noch übrig ist, der Operationsname (Handler, SQL-Query-Name, Outbound-Call-Name) und die Ergebnis-Kategorie (ok, timeout, canceled, anderer Fehler).
Fügen Sie ein paar fokussierte Metriken hinzu, damit das Verhalten unter Last offensichtlich ist:
- Timeout-Anzahl nach Endpoint und Dependency
- Request-Latenz (p50/p95/p99)
- In-Flight-Anfragen
- Datenbank-Query-Latenz (p95/p99)
- Fehlerquote nach Typ
Wenn Sie Fehler behandeln, taggen Sie sie korrekt. context.DeadlineExceeded bedeutet meist, dass Sie Ihr Budget erreicht haben. context.Canceled heißt oft, dass der Client weggegangen ist oder ein Upstream-Timeout zuerst ausgelöst hat. Halten Sie diese getrennt, weil die Lösungen unterschiedlich sind.
Tracing: den Zeitfresser finden
Tracing-Spans sollten demselben Kontext vom HTTP-Handler in database/sql-Aufrufe wie QueryContext folgen. Wenn z. B. eine Anfrage nach 2 Sekunden timed out und der Trace 1,8 Sekunden Wartezeit auf eine DB-Verbindung zeigt, deutet das auf Pool-Größe oder langsame Transaktionen hin, nicht auf den Abfrage-Text.
Wenn Sie ein internes Dashboard dafür bauen (Timeouts nach Route, Top-Langläufer-Abfragen), kann ein No-Code-Tool wie AppMaster helfen, es schnell auszurollen, ohne Observability zu einem separaten Engineering-Projekt zu machen.
Häufige Fehler, die Timeouts wirkungslos machen
Die meisten „es hängt manchmal noch“ Bugs kommen von ein paar kleinen Fehlern.
- Die Uhr unterwegs zurücksetzen. Ein Handler setzt 2s Deadline, aber das Repository erzeugt einen neuen Kontext mit eigener Timeout (oder ohne). Jetzt kann die Datenbank weiterlaufen, nachdem der Client weg ist. Reichen Sie den eingehenden
ctxdurch und straffen Sie ihn nur, wenn Sie einen klaren Grund haben. - Goroutinen starten, die nie stoppen. Arbeit mit
context.Background()starten (oder den ctx ganz fallenlassen) bedeutet, dass sie weiterläuft, auch nachdem die Anfrage abgebrochen wurde. Geben Sie Goroutinen den Request-ctxundselecten Sie aufctx.Done(). - Deadlines zu knapp für echten Traffic. Ein 50ms-Timeout mag auf Ihrem Laptop passen, in Produktion bei einer kleinen Spitze aber versagen und Retries verursachen, die mehr Last erzeugen. Wählen Sie Timeouts basierend auf normaler Latenz plus etwas Puffer.
- Den echten Fehler verbergen.
context.DeadlineExceededals generischen 500er zu behandeln, erschwert Debugging und führt zu falschem Client-Verhalten. Mappen Sie es auf eine klare Timeout-Antwort und loggen Sie den Unterschied zwischen „vom Client abgebrochen“ und „auf Timeout gelaufen". - Ressourcen bei frühem Exit offenlassen. Wenn Sie früh zurückkehren, stellen Sie sicher, dass Sie dennoch
defer rows.Close()ausführen und den Cancel-Funktion auscontext.WithTimeoutaufrufen. Gelöschte Rows oder weiterlaufende Arbeit können Verbindungen unter Last erschöpfen.
Ein kurzes Beispiel: Ein Endpoint startet eine Report-Abfrage. Wenn der Nutzer den Tab schließt, wird der Handler-ctx abgebrochen. Wenn Ihr SQL-Aufruf einen neuen Background-Kontext verwendet hat, läuft die Abfrage weiter, belegt eine Verbindung und verlangsamt alle anderen. Wenn Sie denselben ctx in QueryContext weiterreichen, wird die DB-Abfrage unterbrochen und das System erholt sich schneller.
Schnell-Checkliste für verlässliches Timeout-Verhalten
Timeouts helfen nur, wenn sie konsistent sind. Ein einziger verpasster Aufruf kann eine Goroutine binden, eine DB-Verbindung halten und die nächsten Anfragen verlangsamen.
- Setzen Sie eine klare Deadline am Rand (in der Regel der HTTP-Handler). Alles innerhalb der Anfrage sollte sie erben.
- Reichen Sie denselben
ctxdurch Service- und Repository-Schichten. Vermeiden Siecontext.Background()in Request-Code. - Verwenden Sie kontextbewusste DB-Methoden überall:
QueryContext,QueryRowContext,ExecContext. - Hängen Sie denselben
ctxan ausgehende Aufrufe (HTTP-Clients, Caches, Queues). Wenn Sie einen Child-Context erstellen, halten Sie ihn kürzer, nicht länger. - Handhaben Sie Cancelations und Timeouts konsistent: geben Sie einen sauberen Fehler zurück, stoppen Sie Arbeit und vermeiden Sie Retry-Schleifen innerhalb einer abgebrochenen Anfrage.
Danach verifizieren Sie das Verhalten unter Last. Ein Timeout, das zwar ausgelöst wird, aber Ressourcen nicht schnell genug freigibt, schadet trotzdem der Zuverlässigkeit.
Dashboards sollten Timeouts sichtbar machen, nicht in Averages verschwinden lassen. Verfolgen Sie Signale, die die Frage beantworten: Werden Deadlines wirklich durchgesetzt? Dazu gehören: Request-Timeouts und DB-Timeouts separat, Latenz-Perzentile (p95, p99), DB-Pool-Statistiken (in-use connections, wait count, wait duration) und eine Aufschlüsselung der Fehlerursachen (context deadline exceeded vs. andere Fehler).
Wenn Sie interne Tools auf einer Plattform wie AppMaster bauen, gilt dieselbe Checkliste für alle angebundenen Go-Services: Deadline am Rand definieren, weiterreichen und in Metriken überprüfen, dass festhängende Anfragen zu schnellen Fehlern werden statt zu langsamen Aufläufen.
Beispiel-Szenario und nächste Schritte
Ein häufiger Fall, wo das viel bringt, ist ein Search-Endpoint. Stellen Sie sich GET /search?q=printer vor, der langsamer wird, während die Datenbank durch eine große Report-Abfrage beschäftigt ist. Ohne Deadline kann jede eingehende Anfrage lange auf eine große SQL-Abfrage warten. Unter Last sammeln sich diese feststeckenden Anfragen, blockieren Worker-Goroutinen und Verbindungen, und die ganze API wirkt eingefroren.
Mit einer klaren Deadline im HTTP-Handler und demselben ctx, der an das Repository weitergereicht wird, hört das System auf zu warten, wenn das Budget aufgebraucht ist. Wenn die Deadline greift, bricht der Datenbank-Treiber die Abfrage (sofern unterstützt), der Handler gibt zurück und der Server kann neue Anfragen bedienen, statt ewig auf alte zu warten.
Für den Nutzer ist das Verhalten sichtbar besser, wenn etwas schiefgeht. Statt 30–120 Sekunden zu hängen und dann chaotisch zu scheitern, bekommt der Client einen schnellen, vorhersehbaren Fehler (oft 504 oder 503 mit einer kurzen Meldung wie "request timed out"). Noch wichtiger: das System erholt sich schneller, weil neue Anfragen nicht hinter alten blockieren.
Nächste Schritte, um das über Endpunkte und Teams hinweg durchzusetzen:
- Legen Sie Standard-Timeouts nach Endpunkttyp fest (Search vs. Writes vs. Exports).
- Fordern Sie in Code-Reviews
QueryContextundExecContextein. - Machen Sie Timeout-Fehler am Rand explizit (klarer Statuscode, einfache Nachricht).
- Fügen Sie Metriken für Timeouts und Abbrüche hinzu, damit Regressionen früh auffallen.
- Schreiben Sie einen Helper, der Context-Erzeugung und Logging kapselt, sodass alle Handler gleich reagieren.
Wenn Sie Services und interne Tools mit AppMaster bauen, können Sie diese Timeout-Regeln konsistent auf generierte Go-Backends, API-Integrationen und Dashboards anwenden. AppMaster ist unter appmaster.io verfügbar (No-Code, mit echter Go-Quellcodegenerierung) und kann praktisch sein, wenn Sie konsistentes Request-Handling und Observability wollen, ohne jedes Admin-Tool von Grund auf zu bauen.
FAQ
Eine Anfrage ist „steckengeblieben“, wenn sie auf etwas wartet, das nicht zurückkommt — zum Beispiel eine langsame SQL-Abfrage, eine blockierte Verbindung aus dem Pool, DNS-Probleme oder ein Upstream-Dienst, der nie antwortet. Unter Last häufen sich solche Anfragen, binden Worker und Verbindungen und können eine kleine Verlangsamung in einen größeren Ausfall verwandeln.
Setzen Sie die Gesamtdauer direkt an der HTTP-Grenze und geben Sie denselben ctx an jede Schicht weiter, die blockieren kann. Eine geteilte Deadline verhindert, dass ein paar langsame Operationen Ressourcen lange genug binden, um überall Timeouts auszulösen.
Verwenden Sie ctx, cancel := context.WithTimeout(r.Context(), d) und rufen Sie im Handler (oder Middleware) immer defer cancel() auf. Der Aufruf von cancel gibt Timer frei und hilft, Wartezustände schnell zu beenden, wenn die Anfrage früher fertig ist.
Ersetzen Sie den Request-Kontext nicht durch context.Background() oder context.TODO() in Request-Code — das bricht Cancelation und Deadlines. Wenn Sie den Request-Kontext fallenlassen, kann nachgelagerte Arbeit wie SQL- oder HTTP-Aufrufe weiterlaufen, obwohl der Client bereits weg ist.
Behandeln Sie context.DeadlineExceeded und context.Canceled als normale Kontrollpfade und geben Sie sie unverändert weiter. Am Rand (Handler) sollten Sie sie in klare Antworten übersetzen (oft 504 für Timeouts), damit Clients nicht blind auf scheinbare 500er-Antworten erneut versuchen.
Verwenden Sie überall die kontextbewussten Methoden: QueryContext, QueryRowContext, ExecContext und PrepareContext. Wenn Sie Query() oder Exec() ohne Kontext aufrufen, kann Ihr Handler timeouts auslösen, aber der Datenbank-Aufruf blockiert weiterhin die Goroutine und hält eine Verbindung.
Viele Treiber unterstützen das, aber prüfen Sie es in Ihrem Stack: führen Sie eine absichtlich langsame Abfrage aus und schauen Sie, ob sie nach Ablauf der Deadline schnell abbricht. Als zusätzliche Absicherung ist es sinnvoll, auch eine serverseitige Statement-Timeout-Einstellung in der Datenbank zu verwenden.
Erzeugen Sie die Anfrage mit http.NewRequestWithContext(ctx, ...), damit Deadline und Cancelation automatisch weitergegeben werden. Zusätzlich sollten Sie den http.Client und Transport timeouts konfigurieren (Dial, TLS-Handschlag, Response Header) als harte Obergrenze für den Fall, dass irgendwo versehentlich ein Background-Kontext verwendet wird oder DNS/TLS/Idle-Verbindungen hängen bleiben.
Vermeiden Sie, dass tiefere Schichten frische Kontexte erzeugen, die das Zeitbudget verlängern; Child-Timeouts sollten kürzer sein, nicht länger. Wenn für die Anfrage nur noch wenig Zeit bleibt, überspringen Sie optionale Downstream-Aufrufe, liefern Sie partielle Daten oder schlagen Sie schnell mit einer klaren Fehlermeldung fehl.
Beobachten Sie Timeouts und Abbrüche getrennt nach Endpoint und Abhängigkeit sowie Latenz-Perzentile und In-Flight-Anfragen. In Traces sollte der selbe Kontext vom Handler in Outbound-Aufrufe und QueryContext hineinreichen, damit Sie sehen können, ob Zeit beim Warten auf eine DB-Verbindung, beim Ausführen einer Abfrage oder beim Blockieren auf einen anderen Dienst verloren ging.


