Go Context Deadline Timeout
Definisi
context.Context adalah carrier untuk batas waktu, sinyal pembatalan, dan nilai request-scoped yang perlu ikut mengalir lewat beberapa layer program. Di Go, context dipakai untuk menjaga pekerjaan tetap relevan dengan request atau job yang sedang berjalan.
Context bukan tempat menyimpan state bisnis utama. Ia dipakai untuk hal lintas lapisan yang sifatnya sementara: request ID, auth metadata, deadline, cancellation signal, dan kontrol lifetime pekerjaan.
Kapan context dipakai
Pakai context kalau fungsi atau flow kamu perlu salah satu dari hal berikut:
- bisa dibatalkan saat client putus koneksi
- harus berhenti saat deadline tercapai
- perlu timeout yang konsisten dari atas ke bawah
- berjalan lintas beberapa layer: handler → service → repository → client eksternal
- menjalankan worker atau goroutine yang harus ikut berhenti saat job selesai atau dibatalkan
Cocok untuk context
- HTTP request lifecycle
- database query
- call ke service eksternal
- background worker per job
- pipeline yang butuh stop signal
- tracing / request ID / correlation ID
Kurang cocok untuk context
- menyimpan data bisnis inti seperti model, DTO, atau cache payload
- sebagai parameter pengganti semua dependency
- config aplikasi yang sifatnya statis
- data yang sebenarnya lebih jelas lewat argumen eksplisit
Aturan praktisnya sederhana: kalau nilainya melekat pada umur request/job, context masuk akal. Kalau nilainya adalah input bisnis, kirimkan sebagai argumen biasa.
Prinsip dasar pemakaian
Di Go, context biasanya mengikuti pola ini:
context.Background()ataucontext.TODO()dipakai di titik akar- turunan context dibuat dengan
WithCancel,WithTimeout, atauWithDeadline - context yang sama dipropagasi ke fungsi bawahannya
- receiver atau worker membaca
ctx.Done()untuk berhenti cepat
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()Dua hal penting di sini:
cancel()tetap perlu dipanggil, walau timeout akan memutus context otomatis.- context turunan harus diturunkan lagi ke semua operasi yang relevan.
Cancellation, deadline, dan timeout
Tiga konsep ini sering dicampur, padahal bedanya penting.
Cancellation
Cancellation adalah sinyal eksplisit bahwa pekerjaan harus berhenti.
Biasanya terjadi saat:
- handler selesai lebih awal
- client membatalkan request
- worker menerima stop signal
- parent context dibatalkan
Listener utamanya adalah <-ctx.Done().
Deadline
Deadline adalah waktu absolut kapan context dianggap tidak valid lagi.
Contoh:
ctx, cancel := context.WithDeadline(parent, time.Now().Add(3*time.Second))
defer cancel()Deadline cocok kalau kamu ingin menyatakan: “selesaikan sebelum timestamp ini”.
Timeout
Timeout adalah cara praktis untuk membuat deadline relatif dari sekarang.
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()Timeout lebih sering dipakai di aplikasi harian karena lebih mudah dibaca.
Ringkasnya
- Cancel = stop sekarang
- Deadline = stop sebelum waktu tertentu
- Timeout = deadline relatif dari waktu sekarang
Cara kerja propagasi context
Context harus mengalir dari titik awal request sampai ke operasi terendah yang masih perlu kontrol lifetime.
Urutan yang sehat biasanya begini:
- request masuk ke HTTP handler atau job runner
- handler/job runner menerima
ctx - service layer meneruskan
ctx - repository atau client eksternal memakai
ctxsaat I/O - worker memantau
ctx.Done()untuk berhenti
Diagram ini menunjukkan satu prinsip penting: context bukan cuma “dibawa”, tapi juga dipatuhi.
Propagasi di HTTP flow
Di HTTP server Go, *http.Request sudah membawa context lewat r.Context().
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// teruskan ctx ke service/repository/client
}Alur yang umum
- request masuk ke handler
- handler ambil
ctx := r.Context() - handler panggil service dengan ctx
- service panggil repository atau client dengan ctx
- jika client disconnect, context request biasanya ikut selesai
Contoh pola handler
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := h.UserService.FindByID(ctx, r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = user
}Catatan penting untuk HTTP
- jangan bikin context baru tanpa alasan kalau
r.Context()sudah cukup - kalau butuh batas waktu tambahan, buat turunan dari
r.Context(), bukan dariBackground() - jika handler selesai atau client memutus koneksi, downstream work harus bisa berhenti
Propagasi di server, service, dan repository
Pola yang sehat adalah menerima context sebagai argumen pertama.
func (s *UserService) FindByID(ctx context.Context, id string) (User, error) {
return s.repo.FindByID(ctx, id)
}
func (r *UserRepo) FindByID(ctx context.Context, id string) (User, error) {
// query DB dengan ctx
}Kenapa argumen pertama?
- mudah dipindai
- konsisten dengan idiom Go
- menegaskan bahwa lifetime call dikontrol dari luar
Kalau ada dependency eksternal, pass context sampai ke operasi yang benar-benar bisa block:
- query DB
- HTTP client request
- gRPC call
- message broker publish/consume
- filesystem atau storage call yang mendukung ctx
Jangan memotong context di tengah
Kesalahan yang sering muncul adalah membuat context baru di service tanpa meneruskan parent:
ctx := context.Background()Ini memutus hubungan dengan request aslinya. Akibatnya:
- cancellation dari client hilang
- deadline dari atas tidak ikut turun
- tracing dan metadata request bisa putus
Kalau memang perlu nilai tambahan, gunakan turunan:
ctx = context.WithValue(ctx, key, value)Tapi batasi WithValue untuk metadata lintas layer yang benar-benar perlu, bukan untuk parameter bisnis.
Propagasi di worker flow
Worker sering hidup lebih lama dari HTTP handler, jadi batas hidupnya harus jelas.
Pola worker yang baik
- parent membuat context
- worker menerima context itu
- worker cek
ctx.Done()di loop atau sebelum blocking call - saat job selesai, context dibatalkan atau scope-nya berakhir
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
_ = job
}
}
}Kapan worker butuh context
- background job per request
- worker pool yang harus stop saat shutdown
- retry loop yang harus berhenti saat parent dibatalkan
- fan-out task yang dibatasi deadline
Jebakan worker
Kalau worker tidak memantau ctx.Done(), dia bisa:
- terus hidup setelah request selesai
- menahan goroutine leak
- tetap memanggil dependency walau hasilnya sudah tidak dipakai
Timeout ke dependency eksternal
Context paling berguna saat dipakai di boundary I/O.
Contoh umum:
db.QueryContext(ctx, ...)http.NewRequestWithContext(ctx, ...)grpc.ClientConn.Invoke(...)dengan ctx
Prinsipnya
- timeout di level atas menjaga request tidak kelamaan
- timeout di dependency mencegah operasi block terlalu lama
- jangan memberi timeout pendek di semua layer tanpa alasan, karena bisa menumpuk dan jadi terlalu agresif
Kalau layer atas sudah punya deadline, turunkan context yang sama ke bawah. Jangan bikin timeout berbeda-beda tanpa strategi, karena debugging akan sulit saat request gagal lebih cepat dari yang diduga.
Jebakan umum
1. Menyimpan context di struct
Jangan simpan context.Context di struct sebagai state jangka panjang.
type Service struct {
ctx context.Context // hindari
}Kenapa buruk:
- context bersifat request-scoped, bukan object-scoped
- struct biasanya hidup lebih lama dari request
- gampang memakai context yang sudah expired atau dibatalkan
Lebih baik kirim context per method call.
2. Lupa memanggil cancel
Kalau pakai WithCancel, WithTimeout, atau WithDeadline, panggil cancel().
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()Kenapa penting:
- melepaskan resource internal lebih cepat
- mencegah timer/context child menggantung lebih lama dari perlu
- menjaga lifetime lebih jelas
3. Menggunakan Background() di tengah flow
context.Background() cocok sebagai akar, bukan untuk memutus lineage request.
Kalau kamu pakai Background() di tengah handler/service, kamu kehilangan cancellation dan deadline dari parent.
4. Menaruh data bisnis ke context
WithValue bukan pengganti parameter function.
Gunakan untuk:
- request ID
- trace ID
- auth principal yang lintas layer
- metadata kecil yang memang request-scoped
Jangan gunakan untuk:
- payload utama
- objek domain besar
- parameter yang seharusnya eksplisit
5. Tidak mengecek ctx.Err() atau ctx.Done()
Context baru berguna kalau code-path memang mematuhinya.
Di loop panjang atau work unit yang bisa berhenti, periksa ctx.Done() atau ctx.Err() supaya stop-nya cepat.
6. Membuat timeout terlalu dalam dan saling bertabrakan
Kalau handler punya timeout 2 detik, lalu service memberi 2 detik lagi, dan repository memberi 2 detik lagi, hasilnya bukan 6 detik aman. Yang terjadi justru batasan jadi sulit diprediksi.
Lebih aman:
- tetapkan deadline di titik terluar
- turunkan context yang sama ke bawah
- hanya buat turunan baru jika ada kebutuhan spesifik yang jelas
Contoh pola end-to-end
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orderID, err := h.Service.CreateOrder(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = orderID
}
func (s *Service) CreateOrder(ctx context.Context) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return s.repo.Insert(ctx)
}
func (r *Repo) Insert(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.endpoint, nil)
if err != nil {
return "", err
}
_ = req
return "order-123", nil
}Apa yang terjadi di contoh ini
- handler mengambil context dari request
- service menambahkan timeout spesifik untuk operasi create order
- repo memakai context yang sama saat membuat request outbound
- jika timeout habis atau client putus, chain ikut berhenti
Aturan praktis yang aman
- terima
context.Contextsebagai argumen pertama - turunkan context yang sama ke bawah
- pakai
WithTimeout/WithDeadlinehanya saat ada batas yang jelas - selalu
defer cancel()untuk context turunan - jangan simpan context di struct
- jangan pakai
Background()untuk memutus request yang sedang berjalan - pastikan loop/worker memeriksa
ctx.Done()
Referensi terkait
- Flow concurrency, goroutine, channel, dan select ada di Go Concurrency: Goroutines, Channels, Select, Worker Patterns, dan Context Cancellation
- HTTP server dan middleware flow ada di Go HTTP Server, Routing Dasar, Middleware Pattern, dan JSON API
- Timeout dan request lifecycle di HTTP sering nyambung langsung dengan repository dan client eksternal, jadi context ini biasanya dipakai lintas layer