A server-wide WriteTimeout is a useful safety net, but some handlers have different timing needs. A small JSON response and a streaming export shouldn’t necessarily share the same write budget. When the deadline belongs to one response rather than the whole server, Go’s http.ResponseController gives the handler a direct way to set it.
http.ResponseController.SetWriteDeadline applies a write deadline to the current response. It also solves a practical middleware problem: instead of asserting a concrete optional interface at every call site, a handler can ask the controller to find the capability through compatible ResponseWriter wrappers.
Set a write deadline for one HTTP response
Create a controller from the http.ResponseWriter, then set an absolute deadline:
func reportHandler(w http.ResponseWriter, r *http.Request) {
controller := http.NewResponseController(w)
deadline := time.Now().Add(5 * time.Second)
if err := controller.SetWriteDeadline(deadline); err != nil {
if errors.Is(err, http.ErrNotSupported) {
http.Error(w, "response deadlines are unavailable", http.StatusInternalServerError)
return
}
http.Error(w, "could not configure response deadline", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"status":"ready"}`)
}The method takes a time.Time, not a duration. Computing the deadline near the start of the handler makes the intended budget visible and avoids accidentally resetting a five-second timeout every time another chunk is written.
A zero time.Time removes the write deadline when the underlying writer supports that operation:
if err := controller.SetWriteDeadline(time.Time{}); err != nil {
// decide whether clearing the deadline is required for this handler
}Don’t use that casually to erase a deadline established for a reason elsewhere. A handler-specific deadline should be part of a clear timeout policy rather than a way to make a slow operation run indefinitely.
What the write deadline actually limits
SetWriteDeadline controls writing the HTTP response. After the deadline has passed, later writes won’t block waiting on the client, although a write can still appear to succeed when the data fits into an intermediate buffer.
That buffering detail matters. A successful call to Write isn’t proof that the client received the bytes before the deadline. The application writes through several layers: net/http, the connection, and potentially a reverse proxy or other network infrastructure after that. A response deadline bounds server-side write behavior; it isn’t an end-to-end delivery acknowledgement.
The deadline also doesn’t cancel the handler’s other work. If a database query, template render, or remote API call takes thirty seconds, a five-second response write deadline doesn’t automatically stop that operation. Use request contexts and dependency-specific timeouts for work that should stop when its own budget expires.
A useful separation is:
- request context deadlines bound the lifetime of request-scoped work;
- client or database timeouts bound calls to dependencies;
- response write deadlines bound how long response writes may wait on the connection.
Those controls can share the same overall latency budget, but they protect different boundaries.
Why use http.ResponseController instead of a type assertion
Before ResponseController, code that needed an optional ResponseWriter feature commonly used a type assertion. That approach becomes awkward once middleware wraps the writer.
Suppose middleware records the status code:
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}The wrapper still satisfies http.ResponseWriter, but it doesn’t automatically expose every optional method implemented by the writer underneath it. Code that relies on direct type assertions can therefore lose access to capabilities after an otherwise harmless wrapper is introduced.
ResponseController defines a convention for this case. A wrapper can expose an Unwrap method:
func (w *statusWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}Then a handler can keep passing the writer it actually received:
controller := http.NewResponseController(w)
err := controller.SetWriteDeadline(deadline)The controller can follow compatible wrappers to the underlying writer and invoke the supported operation there. This keeps feature discovery in one standard-library mechanism instead of making each handler understand the application’s middleware stack.
Middleware wrappers should preserve controller access
If you maintain middleware that wraps http.ResponseWriter, adding Unwrap is a small compatibility improvement when the wrapper can safely expose its underlying writer.
A logging wrapper might look like this:
type loggingWriter struct {
http.ResponseWriter
bytes int
}
func (w *loggingWriter) Write(p []byte) (int, error) {
n, err := w.ResponseWriter.Write(p)
w.bytes += n
return n, err
}
func (w *loggingWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}Now ResponseController operations can pass through the logging layer without the wrapper manually reimplementing methods such as SetWriteDeadline, Flush, or Hijack.
There is a design caveat here. Don’t add Unwrap if bypassing the wrapper would violate an invariant the middleware is supposed to enforce. A wrapper that deliberately changes or prohibits a lower-level capability may need to implement the relevant controller method itself and define the behavior explicitly.
For ordinary observability wrappers that count bytes, record status codes, or collect timing information, forwarding access is usually the less surprising choice.
Handle http.ErrNotSupported explicitly
Not every ResponseWriter supports write deadlines. When the controller can’t find the requested capability, it returns an error that matches http.ErrNotSupported.
Check it with errors.Is:
err := http.NewResponseController(w).SetWriteDeadline(deadline)
switch {
case err == nil:
// deadline is configured
case errors.Is(err, http.ErrNotSupported):
// choose a fallback policy
default:
// handle another error from the underlying implementation
}Whether unsupported deadline control is fatal depends on the endpoint. For an ordinary API handler, a server-level timeout may be an acceptable fallback. For a handler whose correctness or resource bounds depend on a per-response deadline, continuing silently may defeat the reason the deadline exists.
Avoid code that simply discards the error:
_ = http.NewResponseController(w).SetWriteDeadline(deadline)That turns an operational assumption into a guess. The endpoint may appear protected in one deployment and lose the protection after a server implementation or middleware stack changes.
Test deadline forwarding without a real network timeout
You don’t need a deliberately slow socket to verify that application code asks for the correct deadline. A focused unit test can use a ResponseWriter that records the value passed to SetWriteDeadline.
type deadlineWriter struct {
http.ResponseWriter
deadline time.Time
}
func (w *deadlineWriter) SetWriteDeadline(t time.Time) error {
w.deadline = t
return nil
}
func (w *deadlineWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}Then exercise the controller:
func TestSetWriteDeadline(t *testing.T) {
rw := &deadlineWriter{
ResponseWriter: httptest.NewRecorder(),
}
want := time.Now().Add(time.Second)
controller := http.NewResponseController(rw)
if err := controller.SetWriteDeadline(want); err != nil {
t.Fatal(err)
}
if !rw.deadline.Equal(want) {
t.Fatalf("deadline = %v, want %v", rw.deadline, want)
}
}This test checks the integration point without depending on scheduler timing or a particular TCP stack. A separate integration test is appropriate when you need to verify real connection behavior, but don’t make every unit test wait for a wall-clock timeout.
It’s also worth testing the unsupported path because test response writers don’t necessarily provide every production capability:
func TestSetWriteDeadlineUnsupported(t *testing.T) {
rw := httptest.NewRecorder()
err := http.NewResponseController(rw).
SetWriteDeadline(time.Now().Add(time.Second))
if !errors.Is(err, http.ErrNotSupported) {
t.Fatalf("error = %v, want http.ErrNotSupported", err)
}
}That makes the fallback policy executable instead of leaving it as an untested branch.
Common mistakes with response write deadlines
One mistake is treating a write deadline as a complete request timeout. It only governs response writes. Long-running computation and dependency calls need their own cancellation and timeout behavior.
Another is setting the deadline too late. If the handler spends most of its budget preparing a response and only configures the deadline immediately before Write, the effective request lifetime can be much longer than the number in the code suggests. Decide whether the budget is meant to start when the handler begins, when a streaming phase begins, or at some other explicit boundary.
Be careful with streaming responses too. A single absolute deadline is different from an idle timeout. If you set a deadline ten seconds from now, a healthy stream is still subject to that absolute point unless you deliberately move or clear the deadline. Repeatedly extending a deadline can be valid for an idle-timeout design, but that should be intentional and tested rather than an accidental side effect of a write loop.
Finally, don’t assume middleware is transparent just because it embeds http.ResponseWriter. Optional HTTP capabilities don’t automatically survive arbitrary wrappers. Use the Unwrap convention where appropriate and test the stack that matters to the endpoint.
Choose the timeout at the boundary you need to protect
Per-response write deadlines are most useful when one handler needs a different connection-write policy from the rest of the server. Keep the deadline close to that handler, check http.ErrNotSupported, and make middleware wrappers cooperate with ResponseController when they can safely do so.
If the real problem is slow database work or an upstream request, fix the timeout at that boundary instead. If every endpoint needs the same response limit, server configuration may be simpler. http.ResponseController.SetWriteDeadline earns its place when the response itself needs a specific budget and the handler should own that decision.