본문으로 건너뛰기 Go context: timeouts and cancellation

Go context: timeouts and cancellation

Go context: timeouts and cancellation

이 글의 핵심

How to bound work and propagate cancellation with context.WithTimeout, WithCancel, and WithDeadline in Go—including HTTP server and client examples and common pitfalls.

Introduction

In distributed systems and HTTP services, how long to wait and whether to stop downstream work when a parent step fails are central to reliability. Go standardizes this with context.Context, and packages such as net/http and database/sql are built to accept a per-request context.

This article focuses on wiring timeouts, cancellation, and deadlines in real code. Collecting Go context cancellation and timeout patterns in one place helps align code reviews across a team. It overlaps the series post on context and graceful shutdown, but here we compress API choice and recipes you can attach directly to HTTP servers and clients.

Covered here: differences among WithCancel / WithTimeout / WithDeadline, cancellation propagation rules, combining http.Server and http.Client, and habits that avoid goroutine leaks.


Reality on the job

When you are learning, everything feels neat and theoretical. Production is different: legacy code, tight deadlines, and bugs you did not expect. The ideas here also start as theory, but you only really see why they are designed this way after you apply them in a real project.

What stuck with me was troubleshooting on my first project: I had followed the book, but things still failed for days until a senior’s review showed the issue—and I learned a lot in that process. This article covers not only the theory but also traps you may hit in practice and how to fix them.


Table of contents

  1. Concepts
  2. Hands-on implementation (step by step)
  3. Advanced use
  4. Performance and comparison
  5. Production scenarios
  6. Troubleshooting
  7. Closing thoughts

Concepts

context.Context is an immutable interface that bundles cancellation (Done), deadline (Deadline), and request-scoped values (Value). Pass the same branch down to child functions: when cancellation or a timeout happens in one place, it can reach the whole subtree consistently.

  • Cancellation (WithCancel): the user cancelled the request, or upstream failed and further work would be pointless.
  • Relative timeout (WithTimeout): a duration limit such as “within N seconds from now.”
  • Absolute deadline (WithDeadline): a wall-clock bound such as “before 23:59 today.”

Whenever you create a cancellable context, you must call the cancel function (to avoid leaking resources). The defer cancel() pattern is effectively standard.


Hands-on implementation (step by step)

1) WithTimeout: cap DB and external API calls

package main
import (
	"context"
	"database/sql"
	"errors"
	"time"
)
func UserByID(ctx context.Context, db *sql.DB, id int64) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
	defer cancel()
	var name string
	err := db.QueryRowContext(ctx, `SELECT name FROM users WHERE id = ?`, id).Scan(&name)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return "", err
		}
		return "", err
	}
	return name, nil
}

The caller’s ctx is usually the request root from http.Request.Context().

2) WithCancel: stop remaining workers when one step fails

func work(ctx context.Context, id int) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	errs := make(chan error, 2)
	go func() { errs <- fetchA(ctx, id) }()
	go func() { errs <- fetchB(ctx, id) }()
	for i := 0; i < 2; i++ {
		if err := <-errs; err != nil {
			cancel() // propagate cancellation to other goroutines on failure
			return err
		}
	}
	return nil
}
func fetchA(ctx context.Context, id int) error {
	select {
	case <-time.After(100 * time.Millisecond):
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}
func fetchB(ctx context.Context, id int) error {
	select {
	case <-time.After(200 * time.Millisecond):
		return errors.New("upstream")
	case <-ctx.Done():
		return ctx.Err()
	}
}

3) HTTP server: request context and client timeout

package main
import (
	"context"
	"io"
	"net/http"
	"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/v1/x", nil)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, err.Error(), 502)
		return
	}
	defer resp.Body.Close()
	w.Header().Set("Content-Type", "application/json")
	_, _ = io.Copy(w, resp.Body)
}

http.Client.Timeout caps the entire request (connect, TLS, body), while NewRequestWithContext’s ctx carries cancellation and deadlines. Using both makes it easier to handle slow upstreams and clients that disconnect.

4) Server shutdown: a separate shutdown context

In operations you often separate per-request ctx from process shutdown. For graceful shutdown, see the Shutdown pattern in Go deep dive #09.


Advanced: propagation, values, layers

  • Propagation: when the parent is cancelled, children are cancelled too. To cancel only a subtree, call cancel() on the WithCancel derived for that branch.
  • Values (context.WithValue): use for request-scoped metadata such as trace IDs or auth subjects; keep business inputs as function arguments for easier testing.
  • Nested timeouts: if the outer limit is 5s and an inner call uses 800ms, the shorter bound fires first. Each layer can enforce its own SLA.

Performance and comparison: when to use each API

APIUse caseNotes
WithCancelManual cancellation, early pipeline exitYou must call cancel()
WithTimeout(d)Relative deadline (now + d)Implemented with WithDeadline internally
WithDeadline(t)Absolute timeFits batches and schedules where wall-clock matters

The context types themselves are cheap; performance problems usually come from blocking I/O that never observes cancellation. Most “slowness” issues are really “cancellation never reaches the I/O.”


Production scenarios

  • API gateway: wrap upstream calls with WithTimeout, and rely on r.Context() so work stops when the client disconnects.
  • Batch jobs: use one WithCancel root and pass the same ctx to workers to stop the whole job after one failure.
  • gRPC / DB: use *Context APIs such as PingContext and QueryContext to stay in the same model.

Troubleshooting

Symptom: timeout never triggers
→ Check whether the blocking call ignores the context. Prefer *Context variants in the standard library.

Symptom: leak warnings because cancel was not called
→ Make defer cancel() a habit; it applies to WithTimeout as well.

Symptom: unsure whether Err() is Canceled vs DeadlineExceeded
→ Compare with errors.Is, e.g. errors.Is(err, context.DeadlineExceeded).

Symptom: slow tests
→ Prefer a short WithTimeout over bare context.Background(), or cancel explicitly with a cancellable ctx.


Closing thoughts

context is how Go lets a team express concurrency and deadline policy in one shared style. Choose WithTimeout, WithCancel, and WithDeadline for the situation, and on HTTP design request ctx plus Client timeout together—this removes a lot of hard-to-reproduce “slow leaks” in production. For goroutine and channel basics, continue with goroutines and channels and the REST walkthrough API project.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Use context.WithTimeout, WithCancel, and WithDeadline in Go to bound work and propagate cancellation. HTTP server and cl… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Actix Web Complete Guide — High-Performance Actor-Based Web
  • C++ RAII & Smart Pointers: Ownership and Automatic Cleanup
  • Astro Content Collections Advanced Guide — Schema, Type

이 글에서 다루는 키워드 (관련 검색어)

Go, context, timeout, cancellation, concurrency, HTTP, goroutine 등으로 검색하시면 이 글이 도움이 됩니다.