context.WithDeadline cancels a context at a specific point in time. Unlike WithTimeout, which takes a duration, WithDeadline takes an absolute time.Time, which is the right choice when several operations need to share one final cutoff. This chapter covers the signature, the deadline-shortening rule, how to inspect a deadline, and when to pick WithDeadline over WithTimeout.
The function lives in the standard context package and has this shape:
You pass it a parent context and an absolute time. It returns a new child context and a CancelFunc. The child's Done() channel closes when one of three things happens: the clock reaches the deadline, the parent gets canceled, or you call cancel() yourself. Once the deadline fires, ctx.Err() returns context.DeadlineExceeded.
Here's the smallest useful example. A flash sale ends at a precise wall-clock moment, and the checkout handler refuses to process any order whose work runs past that time.
The work needed 150ms but the deadline was 100ms out, so ctx.Done() fired first and the select picked the cancel branch. errors.Is(err, context.DeadlineExceeded) lets you tell the timeout case apart from a manual cancel.
A few details:
WithDeadline does not start the work. It only sets up the cancellation signal. The work runs in whatever goroutine called the function, and that work is responsible for watching ctx.Done().WithDeadline, the returned context is canceled immediately. ctx.Err() returns DeadlineExceeded on the first read.cancel is mandatory. Even if you "know" the deadline will fire, call cancel (typically via defer) so the runtime can release the internal timer right away.Cost: Skipping defer cancel() ties up the context's internal timer until the deadline arrives, even if the work finished a long time ago. On a busy server, that's wasted goroutine state and an extra timer the runtime keeps tracking. The compiler go vet rule lostcancel will warn you when a cancel from WithDeadline or WithTimeout isn't used.
Every context exposes a Deadline() method:
If the context has a deadline, ok is true and deadline is the absolute time it will fire. If not (a plain context.Background() or a WithCancel context that has no deadline), ok is false and the time value is the zero time.Time{}. This is useful when a function wants to decide whether to attempt the work at all, or how to budget its sub-calls.
The first call sees the full budget. The second call, after a 60ms sleep, has only ~15ms left and refuses to start. RPC clients and database drivers use this pattern: rather than start an operation that can't finish, the code returns early and frees the caller to retry or fail fast.
A small helper used by some libraries:
This avoids wasting an HTTP round-trip when there isn't enough time left for the response to arrive.
A child context can never have a later deadline than its parent. If you call WithDeadline with a deadline that's further in the future than the parent's, the runtime silently clamps it to the parent's deadline.
Child A asks for an earlier deadline than the parent and gets exactly what it asked for. Child B asks for a later deadline and is silently clamped to the parent's T+10s. Child C doesn't set its own deadline at all and inherits the parent's. The rule is simple: the effective deadline is the earliest of all ancestors and the current node's own deadline.
This is by design. If a downstream call could extend its own deadline past its caller's, a slow leaf could keep the whole call tree alive past when the original caller gave up. Cancellation in Go is meant to flow downward, not be overridden from below.
Here's a runnable demo:
The child "asked for" 500ms but got 100ms because that's all the parent had. child.Deadline() returns the clamped value, so you can always inspect what the context is actually working with.
What's wrong with this code?
The developer expected the job to run for up to 600ms because the inner WithDeadline asked for 600ms. It doesn't. The parent's 200ms cap wins, and the job is canceled at 200ms with context.DeadlineExceeded. No error message, no warning, just clamped behavior.
Fix:
If the job really needs 600ms, the parent has to give it 600ms. Either widen the parent's deadline, or call WithDeadline against context.Background() (with full understanding that you're now detaching from any upstream cancellation, which is usually a bug). The right answer in most code is: don't try to extend a deadline, fix the budget at the top of the call tree.
WithTimeout and WithDeadline do the same job internally. In fact, WithTimeout(parent, d) is documented as equivalent to WithDeadline(parent, time.Now().Add(d)). The difference is in how you express the cutoff.
If each individual call should get its own clock, use WithTimeout. If a chain of calls needs to share a single cutoff, use WithDeadline once at the top and let every step inherit it. The second pattern is the one that prevents the "each retry got 5 seconds, the whole request took 25 seconds" bug.
A side-by-side comparison:
| Aspect | WithTimeout | WithDeadline |
|---|---|---|
| Signature | WithTimeout(parent, time.Duration) | WithDeadline(parent, time.Time) |
| Cutoff expressed as | Duration relative to now | Absolute wall-clock time |
| When the clock starts | At the call site | Fixed regardless of when you call |
| Typical use | Per-call timeout (single RPC, single DB query) | Request-wide budget shared by many calls |
| Risk if reused naively | Each call gets a fresh budget, total time can balloon | None, the deadline is absolute |
| Pairs well with | A wrapped client method | The top of an HTTP/gRPC handler |
Use WithDeadline when:
X-Request-Deadline header or gRPC deadline metadata).Use WithTimeout when:
Here's the pattern that uses WithDeadline correctly. A request-wide budget is set once and shared by every step:
The three steps add up to about 190ms of work, but the deadline is 150ms. The first two steps finish, but reserveStock starts with only ~10ms left, so it sees ctx.Done() close and returns DeadlineExceeded. The whole pipeline shares one absolute cutoff. With WithTimeout(ctx, 150*time.Millisecond) applied at each step, the same code would have run for ~190ms total without complaining, which is exactly the kind of budget creep WithDeadline prevents.
Two patterns come up often. The first is a flash sale: a sale ends at a precise time across all servers, and any order that can't finish before that moment must be rejected so the sale's revenue accounting stays clean.
The deadline is shared. Orders 1 and 2 squeeze in. Order 3 starts but the deadline trips before it finishes. Order 4 doesn't get a chance because the context is already canceled when its turn comes. This is the right behavior: the sale's cutoff is sacred, and any order processed after the deadline would be miscounted by the analytics pipeline.
The second pattern is an order-processing pipeline that must finish a request by some absolute time, regardless of which step is the slow one. We already saw this with placeOrder above. The deadline doesn't reset between steps. If validation takes longer than expected, payment and stock reservation get less time, but the overall promise to the customer ("your order will be confirmed within 200ms") is upheld.
Here's a variation that uses a deadline received from upstream. Imagine an HTTP handler that parses a deadline out of an incoming header (a real gRPC server does this automatically; over HTTP you'd typically send a custom header):
The server doesn't decide the deadline. The client does, and the server honors it. If the client's deadline is shorter than what the server estimates is needed, the server can return early without wasting work, which is what good distributed systems do.
10 quizzes