Skip to content

Add lightweight write deadlines to Conn #572

Description

@kaptinlin

Problem

Long-lived WebSocket servers often need every outbound write to be bounded. With v1.8.15, the main API requires creating a deadline context for every message:

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"github.com/coder/websocket"
)

func main() {
	http.HandleFunc("/", serve)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func serve(w http.ResponseWriter, r *http.Request) {
	c, err := websocket.Accept(w, r, nil)
	if err != nil {
		return
	}
	defer c.CloseNow()

	connCtx := c.CloseRead(r.Context())
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-connCtx.Done():
			return
		case <-ticker.C:
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			err := c.Write(ctx, websocket.MessageText, []byte(`{"type":"event"}`))
			cancel()
			if err != nil {
				return
			}
		}
	}
}

Using context.Background() avoids the timeout-related allocations added around each frame, but also removes the bounded-write guarantee.

websocket.NetConn exposes SetWriteDeadline, but it is a full bidirectional adapter. It creates read and write contexts, timers, and locks, and calls SetReadLimit(-1) even when only its write side is needed.

A loopback benchmark with v1.8.15 on Go 1.27rc2 produced:

Strategy Write path Connection construction
context.WithTimeout 1304 B/op, 12 allocs/op no extra adapter
NetConn.SetWriteDeadline 664 B/op, 5 allocs/op 848 B, 13 allocs

The write measurements use the same client reader, so the allocation difference is the relevant result. At high connection counts, the fixed NetConn cost is material.

Request

Could *websocket.Conn provide a lightweight write-only deadline mechanism, for example SetWriteDeadline(time.Time) or an equivalent API?

Ideally it would:

  • reuse connection-owned deadline state instead of allocating a timer context per write;
  • bound both write-lock acquisition and the underlying write;
  • preserve the current behavior where a timed-out write closes the WebSocket;
  • remain safe with concurrent Ping, Close, and data writes;
  • avoid creating read-side state or changing the configured read limit;
  • allow zero time to clear the deadline.

Related: #252 discusses the same API direction for read deadlines.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions