> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Go WithoutCancel Kept the Values. Where Did the Deadline Go?
- URL: https://debugly.dev/go-context-withoutcancel-lost-deadline/
- Published: 2026-11-08T22:42:00.000Z
- Updated: 2026-11-08T22:42:00.000Z
- Description: Go’s WithoutCancel keeps request values but drops the deadline. Check what the downstream call sees and give detached work its own budget.
- Author: Rohit Bhadani
- Tags: Backend, Concurrency, Debugging

The trace ID made it into your Go cleanup call. The deadline didn't. That's what `context.WithoutCancel` does: it keeps value lookups connected to the parent, but strips the parent's cancellation and deadline. If the call should survive the HTTP handler and still have a time limit, give it a fresh deadline with `context.WithTimeout(context.WithoutCancel(r.Context()), budget)`. If it should stop when the handler's context ends, derive from `r.Context()` directly instead.

*This is an AI-prepared, documentation-based explainer, not a personally reproduced incident. The scenario and snippets are illustrative. See the* [*editorial policy*](https://debugly.dev/editorial-policy/)*.*

## A context carries more than one thing

Suppose a handler needs to tell an upstream service to release a temporary reservation. It's reasonable to want the cleanup call to survive a client disconnect. The original context isn't a great fit for that job: for incoming server requests, [Go's HTTP documentation](https://pkg.go.dev/net/http?ref=debugly.dev#Request.Context) says the request context is cancelled when the client connection closes, the request is cancelled with HTTP/2, or `ServeHTTP` returns. But removing that connection also removes the old deadline. The trace ID can look perfectly healthy while the clock has quietly left the building.

The [WithoutCancel contract](https://pkg.go.dev/context?ref=debugly.dev#WithoutCancel) is specific: no deadline, no error, a nil `Done()` channel and no cancellation cause. Values can still be resolved through the parent, as the [standard-library implementation](https://go.dev/src/context/context.go?ref=debugly.dev) shows. This isn't a copy of the request context with one flag flipped. It changes which signals a downstream operation can observe.

## Check the context at the call site

At the boundary where you start the cleanup, inspect the context that actually reaches the client. Check the boolean from `Deadline()` and whether `Done()` is nil. Don't infer either from a trace ID, and don't dump context values into logs. A nil channel is an important clue: the [Go specification](https://go.dev/ref/spec?ref=debugly.dev#Select%5Fstatements) says a case on a nil channel can't proceed. In a `select` with other cases, that cancellation case is disabled. A bare receive from that channel can wait forever.

A separate HTTP client timeout may still bound the call. Check it before diagnosing a hang, but remember that the context's contract remains the same even if another timer saves you. In the illustrative helper below, `cleanup` must pass the context to the actual operation, such as an outgoing request built with `http.NewRequestWithContext`. A timer on a context can't stop code that ignores it.

```
func releaseAfterRequest(
    requestCtx context.Context,
    cleanup func(context.Context) error,
) error {
    base := context.WithoutCancel(requestCtx)
    ctx, cancel := context.WithTimeout(base, 2*time.Second)
    defer cancel()
    return cleanup(ctx)
}
```

The two seconds are an example cleanup budget, not a recommendation for your service. [WithTimeout](https://pkg.go.dev/context?ref=debugly.dev#WithTimeout) sets a new deadline on the detached context. It doesn't restore the old deadline. If policy requires respecting the original absolute deadline even after detaching cancellation, read that deadline from the parent and explicitly choose the earlier of it and the cleanup budget. An already expired deadline should stay expired under that policy.

## Put the budget where the work is owned

This helper runs synchronously. If you move the operation into a goroutine, make the goroutine or its supervisor own the new context and its cancel function. Creating the timeout in the handler and deferring cancellation there can cancel the cleanup as soon as the handler returns. For work that has to survive a process crash, use a durable job mechanism. Detaching a context doesn't make an in-memory goroutine durable. Consider passing just the correlation data the worker needs rather than retaining all the request's values.

In a test, cancel the parent first and then start the detached operation against a controlled client. Assert that the new context still exposes the expected correlation value and that it reaches its own deadline. In a separate request-owned test, assert that cancellation of the parent still reaches the call. Debugly's [Node.js request disconnect explainer](https://debugly.dev/nodejs-request-signal-cancel-fetch-client-disconnect/) covers the complementary mistake: forgetting to pass cancellation into work that should stop with the request. Here you're removing that connection on purpose. The test should prove what replaces it.