Step 01 of 06
Learn the concept
A monitor with no deadline is just a goroutine waiting to become folklore. One stuck DNS query or silent peer can hold a worker forever, and schedules do not forgive forever. Beacon needs a worst case it can say out loud: five seconds, then a result.
The ideas this is made of
A deadline is the promise the scheduler can use
Five separate five-second timeouts can produce a 25-second probe. That may be correct per operation and useless operationally. An end-to-end deadline says the whole check stops by one instant, no matter where the time went. The scheduler can plan capacity around that maximum, and the result can honestly say the target consumed the full budget.
Context cancellation is a tree, not a global variable
context.WithTimeout(parent, 5*time.Second) returns a child context and a cancel function. When the child is cancelled, operations using it observe <-ctx.Done() and ctx.Err(). Children derived from it cancel too. Call defer cancel() immediately. It releases timer resources early when work finishes and avoids a quiet leak in loops.
A deadline only works where you pass it
DNS needs LookupIPAddr(ctx, host). TCP needs net.Dialer.DialContext. Manual TLS needs tls.Conn.HandshakeContext. HTTP needs http.NewRequestWithContext. Leave one blocking layer on context.Background() and that layer can outlive the check. Context is not atmospheric. It must be threaded through every wait, retry and cleanup path.
Deadline errors are values with wrappers around them
When a deadline expires, the category is context.DeadlineExceeded. Lower layers may wrap it with text such as Client.Timeout exceeded while awaiting headers. Use errors.Is(err, context.DeadlineExceeded) and check ctx.Err(). String matching breaks when Go, the OS or a dependency changes wording during exactly the incident you are debugging.
`http.Client.Timeout` is broad, but not enough here
http.Client.Timeout limits a request made by that client. It is useful and blunt. Beacon already measures DNS, TCP and TLS explicitly, so the budget must be visible above all of them. A context deadline composes with process shutdown, parent cancellation and manual layer timing. It is the one clock every layer can share.
package main
import (
"context"
"errors"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("request exceeded its deadline")
return
}
panic(err)
}
defer resp.Body.Close()
fmt.Println("status:", resp.StatusCode)
}The deadline is created once, cancelled exactly once, and attached to the request. Beacon extends the same pattern down into DNS, TCP and TLS so one budget covers the whole journey.
Three timeout tools people confuse
| Tool | What it means | Best use |
|---|---|---|
WithTimeout | Duration from now | One check budget |
WithDeadline | Specific instant | Shared external cutoff |
Client.Timeout | Whole HTTP request | Simple client policy |
Dialer.Timeout | Connect only | Narrow TCP cap |
What these are called on the job
Deadline — An absolute instant after which work should stop.
Timeout — A duration used to derive a deadline.
Cancellation — A signal that callers no longer want the work.
Context propagation — Passing the same context to every API that may block.
Timer leak — Resources left by creating timeout contexts without cancelling them.
