An HTTP handler can write bytes without making those bytes immediately visible to the client. The server, transport, middleware, or another layer may buffer response data. For handlers that emit incremental output, http.ResponseController.Flush provides an explicit request to push buffered data toward the client.

The operation belongs to the current response. It does not turn a normal handler into a separate transport protocol, and it does not guarantee that every intermediary on the network will forward each chunk at the same instant. Its useful contract is narrower: ask the active response writer to flush data it has buffered.

Flush through the response controller

A handler can create a controller from the http.ResponseWriter it received and call Flush after writing a logical unit:

func streamHandler(w http.ResponseWriter, r *http.Request) {
    controller := http.NewResponseController(w)

    w.Header().Set("Content-Type", "text/plain; charset=utf-8")

    for _, part := range []string{"alpha\n", "beta\n", "gamma\n"} {
        if _, err := io.WriteString(w, part); err != nil {
            return
        }

        if err := controller.Flush(); err != nil {
            return
        }
    }
}

Flush returns an error, unlike the older http.Flusher.Flush method. If the underlying writer does not support flushing, the returned error matches http.ErrNotSupported. Other errors can also come from the writer implementation.

That error channel matters when incremental delivery is part of the response contract. A handler can stop producing more output instead of assuming that a flush occurred.

ResponseController can cross compatible wrappers

Middleware often wraps http.ResponseWriter to record status codes, byte counts, or other response state. Direct interface assertions become fragile when each wrapper must reproduce every optional interface exposed by the writer beneath it.

http.ResponseController has a wrapper convention for this case. A wrapper can expose:

func (w *trackingWriter) Unwrap() http.ResponseWriter {
    return w.ResponseWriter
}

The controller can follow compatible Unwrap methods until it reaches a writer that implements the requested operation. This keeps middleware focused on its own behavior while retaining access to supported response controls.

A wrapper that intentionally changes response semantics still needs care. Unwrap should represent a valid path to the underlying writer only when bypassing the wrapper for controller operations preserves the wrapper’s contract.

A flush does not promise immediate arrival

Flushing removes one source of delay: buffering in the response writer path that supports the operation. It cannot force unrelated buffers outside that path to behave differently.

A reverse proxy may buffer upstream responses. A client library may read data in larger blocks. Network transport can combine writes. Compression middleware may also accumulate input before producing enough encoded output to send.

For that reason, code should not treat one call to Flush as a timing guarantee at the remote application. The call is still useful because it marks the point where the handler has finished one unit and asks the server-side response path to release available buffered data.

Headers become committed once output starts

Streaming handlers have less freedom after the first response bytes are committed. Writing the body normally sends an implicit 200 OK status if no status was written explicitly. A flush can also commit pending headers.

Header values that must be present for the response should therefore be set before the first write or flush:

func eventHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")

    controller := http.NewResponseController(w)

    if _, err := io.WriteString(w, "data: ready\n\n"); err != nil {
        return
    }
    if err := controller.Flush(); err != nil {
        return
    }
}

After the response is committed, changing ordinary headers is too late to alter the header block already sent to the client. This is a general HTTP response rule, but explicit flushing makes the boundary easier to cross earlier in a handler.

Flush frequency affects the response path

Calling Flush after every tiny fragment can create more work across the server and transport stack than flushing at meaningful message boundaries. Holding data for too long defeats the purpose of incremental delivery.

The useful interval comes from the application protocol. A server-sent event is a natural flush boundary. A generated report may flush after a complete record group. A progress stream may flush after each state update rather than after every formatted token.

This is not a fixed performance formula. The response size, middleware, protocol version, proxy behavior, and client all affect the result. The handler should express boundaries that matter to the response rather than using flush calls as arbitrary punctuation.

Cancellation still governs handler work

Flushing does not keep a request alive after its context is canceled. A streaming handler that performs ongoing work should continue to observe r.Context() and stop when the request is no longer active.

func pulseHandler(w http.ResponseWriter, r *http.Request) {
    controller := http.NewResponseController(w)
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-r.Context().Done():
            return
        case t := <-ticker.C:
            if _, err := fmt.Fprintf(w, "%s\n", t.UTC().Format(time.RFC3339)); err != nil {
                return
            }
            if err := controller.Flush(); err != nil {
                return
            }
        }
    }
}

Write and flush errors are also termination signals. Continuing to generate data after the response path has failed wastes work and can hide the condition that ended delivery.

http.ResponseController.Flush is most useful when a response has real incremental boundaries and the handler needs an error-aware way to release buffered output. It gives that control without requiring middleware stacks to preserve a growing set of optional writer interfaces manually, while leaving network-level delivery timing outside the handler’s guarantees.