Repository: quic-go/quic-go
Pull request #4723 (2024-11-15 10:53:56)

enable DPLPMTUD on macOS dual-stack sockets
My new draft in the TSV WG (https://datatracker.ietf.org/doc/draft-seemann-tsvwg-udp-fragmentation/) contains details on how IP fragmentation is handled on different platforms.


Patch:
diff --git a/.github/workflows/go-generate.sh b/.github/workflows/go-generate.sh
index 2d1b9447..fab97c73 100755
--- a/.github/workflows/go-generate.sh
+++ b/.github/workflows/go-generate.sh
@@ -2,22 +2,17 @@
 
 set -e
 
-DIR=$(pwd)
-TMP=$(mktemp -d)
-cd "$TMP"
-cp -r "$DIR" orig
-cp -r "$DIR" generated
-
-cd generated
-# delete all go-generated files generated (that adhere to the comment convention)
-grep --include \*.go -lrIZ "^// Code generated .* DO NOT EDIT\.$" . | xargs --null rm
+# delete all go-generated files (that adhere to the comment convention)
+git ls-files -z | grep --include \*.go -lrIZ "^// Code generated .* DO NOT EDIT\.$" | tr '\0' '\n' | xargs rm -f
 
 # First regenerate sys_conn_buffers_write.go.
 # If it doesn't exist, the following mockgen calls will fail.
 go generate -run "sys_conn_buffers_write.go"
 # now generate everything
 go generate ./...
-cd ..
 
-# don't compare fuzzing corpora
-diff --exclude=corpus --exclude=.git -ruN orig generated
+# Check if any files were changed
+git diff --exit-code || (
+    echo "Generated files are not up to date. Please run 'go generate ./...' and commit the changes."
+    exit 1
+)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a193943d..22fc9a58 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -41,7 +41,7 @@ jobs:
       matrix:
         go: [  "1.22.x", "1.23.x" ]
     env:
-      GOLANGCI_LINT_VERSION: v1.60.1
+      GOLANGCI_LINT_VERSION: v1.62.0
     name: golangci-lint (Go ${{ matrix.go }})
     steps:
       - uses: actions/checkout@v4
diff --git a/.golangci.yml b/.golangci.yml
index 63b40cc3..44052022 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -16,9 +16,9 @@ linters:
   disable-all: true
   enable:
     - asciicheck
+    - copyloopvar
     - depguard
     - exhaustive
-    - exportloopref
     - goimports
     - gofmt # redundant, since gofmt *should* be a no-op after gofumpt
     - gofumpt
@@ -44,3 +44,4 @@ issues:
       linters:
         - exhaustive
         - prealloc
+        - unparam
diff --git a/codecov.yml b/codecov.yml
index 59e4b58f..77e47fbe 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -6,6 +6,8 @@ coverage:
     - internal/handshake/cipher_suite.go
     - internal/utils/linkedlist/linkedlist.go
     - internal/testdata
+    - logging/connection_tracer_multiplexer.go
+    - logging/tracer_multiplexer.go
     - testutils/
     - fuzzing/
     - metrics/
diff --git a/connection.go b/connection.go
index 4390f5ca..8bd6f360 100644
--- a/connection.go
+++ b/connection.go
@@ -1210,7 +1210,7 @@ func (s *connection) handleUnpackedLongHeaderPacket(
 			s.tracer.ReceivedLongHeaderPacket(packet.hdr, packetSize, ecn, frames)
 		}
 	}
-	isAckEliciting, err := s.handleFrames(packet.data, packet.hdr.DestConnectionID, packet.encryptionLevel, log)
+	isAckEliciting, err := s.handleFrames(packet.data, packet.hdr.DestConnectionID, packet.encryptionLevel, log, rcvTime)
 	if err != nil {
 		return err
 	}
@@ -1229,7 +1229,7 @@ func (s *connection) handleUnpackedShortHeaderPacket(
 	s.firstAckElicitingPacketAfterIdleSentTime = time.Time{}
 	s.keepAlivePingSent = false
 
-	isAckEliciting, err := s.handleFrames(data, destConnID, protocol.Encryption1RTT, log)
+	isAckEliciting, err := s.handleFrames(data, destConnID, protocol.Encryption1RTT, log, rcvTime)
 	if err != nil {
 		return err
 	}
@@ -1241,6 +1241,7 @@ func (s *connection) handleFrames(
 	destConnID protocol.ConnectionID,
 	encLevel protocol.EncryptionLevel,
 	log func([]logging.Frame),
+	rcvTime time.Time,
 ) (isAckEliciting bool, _ error) {
 	// Only used for tracing.
 	// If we're not tracing, this slice will always remain empty.
@@ -1270,7 +1271,7 @@ func (s *connection) handleFrames(
 		if handleErr != nil {
 			continue
 		}
-		if err := s.handleFrame(frame, encLevel, destConnID); err != nil {
+		if err := s.handleFrame(frame, encLevel, destConnID, rcvTime); err != nil {
 			if log == nil {
 				return false, err
 			}
@@ -1299,20 +1300,25 @@ func (s *connection) handleFrames(
 	return
 }
 
-func (s *connection) handleFrame(f wire.Frame, encLevel protocol.EncryptionLevel, destConnID protocol.ConnectionID) error {
+func (s *connection) handleFrame(
+	f wire.Frame,
+	encLevel protocol.EncryptionLevel,
+	destConnID protocol.ConnectionID,
+	rcvTime time.Time,
+) error {
 	var err error
 	wire.LogFrame(s.logger, f, false)
 	switch frame := f.(type) {
 	case *wire.CryptoFrame:
 		err = s.handleCryptoFrame(frame, encLevel)
 	case *wire.StreamFrame:
-		err = s.handleStreamFrame(frame)
+		err = s.handleStreamFrame(frame, rcvTime)
 	case *wire.AckFrame:
 		err = s.handleAckFrame(frame, encLevel)
 	case *wire.ConnectionCloseFrame:
 		s.handleConnectionCloseFrame(frame)
 	case *wire.ResetStreamFrame:
-		err = s.handleResetStreamFrame(frame)
+		err = s.handleResetStreamFrame(frame, rcvTime)
 	case *wire.MaxDataFrame:
 		s.handleMaxDataFrame(frame)
 	case *wire.MaxStreamDataFrame:
@@ -1425,7 +1431,7 @@ func (s *connection) handleHandshakeEvents() error {
 	}
 }
 
-func (s *connection) handleStreamFrame(frame *wire.StreamFrame) error {
+func (s *connection) handleStreamFrame(frame *wire.StreamFrame, rcvTime time.Time) error {
 	str, err := s.streamsMap.GetOrOpenReceiveStream(frame.StreamID)
 	if err != nil {
 		return err
@@ -1435,7 +1441,7 @@ func (s *connection) handleStreamFrame(frame *wire.StreamFrame) error {
 		// ignore this StreamFrame
 		return nil
 	}
-	return str.handleStreamFrame(frame)
+	return str.handleStreamFrame(frame, rcvTime)
 }
 
 func (s *connection) handleMaxDataFrame(frame *wire.MaxDataFrame) {
@@ -1459,7 +1465,7 @@ func (s *connection) handleMaxStreamsFrame(frame *wire.MaxStreamsFrame) {
 	s.streamsMap.HandleMaxStreamsFrame(frame)
 }
 
-func (s *connection) handleResetStreamFrame(frame *wire.ResetStreamFrame) error {
+func (s *connection) handleResetStreamFrame(frame *wire.ResetStreamFrame, rcvTime time.Time) error {
 	str, err := s.streamsMap.GetOrOpenReceiveStream(frame.StreamID)
 	if err != nil {
 		return err
@@ -1468,7 +1474,7 @@ func (s *connection) handleResetStreamFrame(frame *wire.ResetStreamFrame) error
 		// stream is closed and already garbage collected
 		return nil
 	}
-	return str.handleResetStreamFrame(frame)
+	return str.handleResetStreamFrame(frame, rcvTime)
 }
 
 func (s *connection) handleStopSendingFrame(frame *wire.StopSendingFrame) error {
@@ -1879,7 +1885,7 @@ func (s *connection) sendPackets(now time.Time) error {
 	if isBlocked, offset := s.connFlowController.IsNewlyBlocked(); isBlocked {
 		s.framer.QueueControlFrame(&wire.DataBlockedFrame{MaximumData: offset})
 	}
-	if offset := s.connFlowController.GetWindowUpdate(); offset > 0 {
+	if offset := s.connFlowController.GetWindowUpdate(now); offset > 0 {
 		s.framer.QueueControlFrame(&wire.MaxDataFrame{MaximumData: offset})
 	}
 	if cf := s.cryptoStreamManager.GetPostHandshakeData(protocol.MaxPostHandshakeCryptoFrameSize); cf != nil {
@@ -1887,7 +1893,7 @@ func (s *connection) sendPackets(now time.Time) error {
 	}
 
 	if !s.handshakeConfirmed {
-		packet, err := s.packer.PackCoalescedPacket(false, s.maxPacketSize(), s.version)
+		packet, err := s.packer.PackCoalescedPacket(false, s.maxPacketSize(), now, s.version)
 		if err != nil || packet == nil {
 			return err
 		}
@@ -2014,7 +2020,7 @@ func (s *connection) resetPacingDeadline() {
 func (s *connection) maybeSendAckOnlyPacket(now time.Time) error {
 	if !s.handshakeConfirmed {
 		ecn := s.sentPacketHandler.ECNMode(false)
-		packet, err := s.packer.PackCoalescedPacket(true, s.maxPacketSize(), s.version)
+		packet, err := s.packer.PackCoalescedPacket(true, s.maxPacketSize(), now, s.version)
 		if err != nil {
 			return err
 		}
@@ -2025,7 +2031,7 @@ func (s *connection) maybeSendAckOnlyPacket(now time.Time) error {
 	}
 
 	ecn := s.sentPacketHandler.ECNMode(true)
-	p, buf, err := s.packer.PackAckOnlyPacket(s.maxPacketSize(), s.version)
+	p, buf, err := s.packer.PackAckOnlyPacket(s.maxPacketSize(), now, s.version)
 	if err != nil {
 		if err == errNothingToPack {
 			return nil
@@ -2047,7 +2053,7 @@ func (s *connection) sendProbePacket(encLevel protocol.EncryptionLevel, now time
 			break
 		}
 		var err error
-		packet, err = s.packer.MaybePackProbePacket(encLevel, s.maxPacketSize(), s.version)
+		packet, err = s.packer.MaybePackProbePacket(encLevel, s.maxPacketSize(), now, s.version)
 		if err != nil {
 			return err
 		}
@@ -2058,7 +2064,7 @@ func (s *connection) sendProbePacket(encLevel protocol.EncryptionLevel, now time
 	if packet == nil {
 		s.retransmissionQueue.AddPing(encLevel)
 		var err error
-		packet, err = s.packer.MaybePackProbePacket(encLevel, s.maxPacketSize(), s.version)
+		packet, err = s.packer.MaybePackProbePacket(encLevel, s.maxPacketSize(), now, s.version)
 		if err != nil {
 			return err
 		}
@@ -2073,7 +2079,7 @@ func (s *connection) sendProbePacket(encLevel protocol.EncryptionLevel, now time
 // If there was nothing to pack, the returned size is 0.
 func (s *connection) appendOneShortHeaderPacket(buf *packetBuffer, maxSize protocol.ByteCount, ecn protocol.ECN, now time.Time) (protocol.ByteCount, error) {
 	startLen := buf.Len()
-	p, err := s.packer.AppendPacket(buf, maxSize, s.version)
+	p, err := s.packer.AppendPacket(buf, maxSize, now, s.version)
 	if err != nil {
 		return 0, err
 	}
diff --git a/framer.go b/framer.go
index e162f6b8..591fd929 100644
--- a/framer.go
+++ b/framer.go
@@ -3,6 +3,7 @@ package quic
 import (
 	"slices"
 	"sync"
+	"time"
 
 	"github.com/quic-go/quic-go/internal/ackhandler"
 	"github.com/quic-go/quic-go/internal/protocol"
@@ -21,7 +22,7 @@ const (
 const maxStreamControlFrameSize = 25
 
 type streamControlFrameGetter interface {
-	getControlFrame() (_ ackhandler.Frame, ok, hasMore bool)
+	getControlFrame(time.Time) (_ ackhandler.Frame, ok, hasMore bool)
 }
 
 type framer struct {
@@ -78,7 +79,12 @@ func (f *framer) QueueControlFrame(frame wire.Frame) {
 	f.controlFrames = append(f.controlFrames, frame)
 }
 
-func (f *framer) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.ByteCount, v protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
+func (f *framer) AppendControlFrames(
+	frames []ackhandler.Frame,
+	maxLen protocol.ByteCount,
+	now time.Time,
+	v protocol.Version,
+) ([]ackhandler.Frame, protocol.ByteCount) {
 	f.controlFrameMutex.Lock()
 	defer f.controlFrameMutex.Unlock()
 
@@ -101,7 +107,7 @@ func (f *framer) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.
 		if remainingLen <= maxStreamControlFrameSize {
 			break
 		}
-		fr, ok, hasMore := str.getControlFrame()
+		fr, ok, hasMore := str.getControlFrame(now)
 		if !hasMore {
 			delete(f.streamsWithControlFrames, id)
 		}
diff --git a/go.mod b/go.mod
index 5e1ee107..416f1f7b 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	golang.org/x/sync v0.8.0
 	golang.org/x/sys v0.23.0
 	golang.org/x/time v0.5.0
+	golang.org/x/tools v0.22.0
 )
 
 require (
@@ -30,9 +31,8 @@ require (
 	github.com/prometheus/client_model v0.5.0 // indirect
 	github.com/prometheus/common v0.48.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
-	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/text v0.17.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
 	google.golang.org/protobuf v1.33.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 25fe9d11..022aa0f4 100644
--- a/go.sum
+++ b/go.sum
@@ -154,8 +154,8 @@ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQ
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -197,8 +197,8 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
diff --git a/internal/flowcontrol/base_flow_controller.go b/internal/flowcontrol/base_flow_controller.go
index 3d88d577..950e5f72 100644
--- a/internal/flowcontrol/base_flow_controller.go
+++ b/internal/flowcontrol/base_flow_controller.go
@@ -36,7 +36,7 @@ type baseFlowController struct {
 // For every offset, it only returns true once.
 // If it is blocked, the offset is returned.
 func (c *baseFlowController) IsNewlyBlocked() (bool, protocol.ByteCount) {
-	if c.sendWindowSize() != 0 || c.sendWindow == c.lastBlockedAt {
+	if c.SendWindowSize() != 0 || c.sendWindow == c.lastBlockedAt {
 		return false, 0
 	}
 	c.lastBlockedAt = c.sendWindow
@@ -56,7 +56,7 @@ func (c *baseFlowController) UpdateSendWindow(offset protocol.ByteCount) (update
 	return false
 }
 
-func (c *baseFlowController) sendWindowSize() protocol.ByteCount {
+func (c *baseFlowController) SendWindowSize() protocol.ByteCount {
 	// this only happens during connection establishment, when data is sent before we receive the peer's transport parameters
 	if c.bytesSent > c.sendWindow {
 		return 0
@@ -66,11 +66,6 @@ func (c *baseFlowController) sendWindowSize() protocol.ByteCount {
 
 // needs to be called with locked mutex
 func (c *baseFlowController) addBytesRead(n protocol.ByteCount) {
-	// pretend we sent a WindowUpdate when reading the first byte
-	// this way auto-tuning of the window size already works for the first WindowUpdate
-	if c.bytesRead == 0 {
-		c.startNewAutoTuningEpoch(time.Now())
-	}
 	c.bytesRead += n
 }
 
@@ -82,19 +77,19 @@ func (c *baseFlowController) hasWindowUpdate() bool {
 
 // getWindowUpdate updates the receive window, if necessary
 // it returns the new offset
-func (c *baseFlowController) getWindowUpdate() protocol.ByteCount {
+func (c *baseFlowController) getWindowUpdate(now time.Time) protocol.ByteCount {
 	if !c.hasWindowUpdate() {
 		return 0
 	}
 
-	c.maybeAdjustWindowSize()
+	c.maybeAdjustWindowSize(now)
 	c.receiveWindow = c.bytesRead + c.receiveWindowSize
 	return c.receiveWindow
 }
 
 // maybeAdjustWindowSize increases the receiveWindowSize if we're sending updates too often.
 // For details about auto-tuning, see https://docs.google.com/document/d/1SExkMmGiz8VYzV3s9E35JQlJ73vhzCekKkDi85F1qCE/edit?usp=sharing.
-func (c *baseFlowController) maybeAdjustWindowSize() {
+func (c *baseFlowController) maybeAdjustWindowSize(now time.Time) {
 	bytesReadInEpoch := c.bytesRead - c.epochStartOffset
 	// don't do anything if less than half the window has been consumed
 	if bytesReadInEpoch <= c.receiveWindowSize/2 {
@@ -106,7 +101,6 @@ func (c *baseFlowController) maybeAdjustWindowSize() {
 	}
 
 	fraction := float64(bytesReadInEpoch) / float64(c.receiveWindowSize)
-	now := time.Now()
 	if now.Sub(c.epochStartTime) < time.Duration(4*fraction*float64(rtt)) {
 		// window is consumed too fast, try to increase the window size
 		newSize := min(2*c.receiveWindowSize, c.maxReceiveWindowSize)
diff --git a/internal/flowcontrol/connection_flow_controller.go b/internal/flowcontrol/connection_flow_controller.go
index 2efcad74..016ffaa9 100644
--- a/internal/flowcontrol/connection_flow_controller.go
+++ b/internal/flowcontrol/connection_flow_controller.go
@@ -24,7 +24,7 @@ func NewConnectionFlowController(
 	allowWindowIncrease func(size protocol.ByteCount) bool,
 	rttStats *utils.RTTStats,
 	logger utils.Logger,
-) ConnectionFlowController {
+) *connectionFlowController {
 	return &connectionFlowController{
 		baseFlowController: baseFlowController{
 			rttStats:             rttStats,
@@ -37,16 +37,17 @@ func NewConnectionFlowController(
 	}
 }
 
-func (c *connectionFlowController) SendWindowSize() protocol.ByteCount {
-	return c.baseFlowController.sendWindowSize()
-}
-
 // IncrementHighestReceived adds an increment to the highestReceived value
-func (c *connectionFlowController) IncrementHighestReceived(increment protocol.ByteCount) error {
+func (c *connectionFlowController) IncrementHighestReceived(increment protocol.ByteCount, now time.Time) error {
 	c.mutex.Lock()
 	defer c.mutex.Unlock()
 
+	// If this is the first frame received on this connection, start flow-control auto-tuning.
+	if c.highestReceived == 0 {
+		c.startNewAutoTuningEpoch(now)
+	}
 	c.highestReceived += increment
+
 	if c.checkFlowControlViolation() {
 		return &qerr.TransportError{
 			ErrorCode:    qerr.FlowControlError,
@@ -62,34 +63,39 @@ func (c *connectionFlowController) AddBytesRead(n protocol.ByteCount) {
 	c.mutex.Unlock()
 }
 
-func (c *connectionFlowController) GetWindowUpdate() protocol.ByteCount {
+func (c *connectionFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount {
 	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate()
+	offset := c.baseFlowController.getWindowUpdate(now)
 	if c.logger.Debug() && oldWindowSize < c.receiveWindowSize {
 		c.logger.Debugf("Increasing receive flow control window for the connection to %d kB", c.receiveWindowSize/(1<<10))
 	}
-	c.mutex.Unlock()
 	return offset
 }
 
 // EnsureMinimumWindowSize sets a minimum window size
 // it should make sure that the connection-level window is increased when a stream-level window grows
-func (c *connectionFlowController) EnsureMinimumWindowSize(inc protocol.ByteCount) {
+func (c *connectionFlowController) EnsureMinimumWindowSize(inc protocol.ByteCount, now time.Time) {
 	c.mutex.Lock()
-	if inc > c.receiveWindowSize {
-		c.logger.Debugf("Increasing receive flow control window for the connection to %d kB, in response to stream flow control window increase", c.receiveWindowSize/(1<<10))
-		newSize := min(inc, c.maxReceiveWindowSize)
-		if delta := newSize - c.receiveWindowSize; delta > 0 && c.allowWindowIncrease(delta) {
-			c.receiveWindowSize = newSize
+	defer c.mutex.Unlock()
+
+	if inc <= c.receiveWindowSize {
+		return
+	}
+	newSize := min(inc, c.maxReceiveWindowSize)
+	if delta := newSize - c.receiveWindowSize; delta > 0 && c.allowWindowIncrease(delta) {
+		c.receiveWindowSize = newSize
+		if c.logger.Debug() {
+			c.logger.Debugf("Increasing receive flow control window for the connection to %d, in response to stream flow control window increase", newSize)
 		}
-		c.startNewAutoTuningEpoch(time.Now())
 	}
-	c.mutex.Unlock()
+	c.startNewAutoTuningEpoch(now)
 }
 
 // Reset rests the flow controller. This happens when 0-RTT is rejected.
-// All stream data is invalidated, it's if we had never opened a stream and never sent any data.
+// All stream data is invalidated, it's as if we had never opened a stream and never sent any data.
 // At that point, we only have sent stream data, but we didn't have the keys to open 1-RTT keys yet.
 func (c *connectionFlowController) Reset() error {
 	c.mutex.Lock()
diff --git a/internal/flowcontrol/interface.go b/internal/flowcontrol/interface.go
index 57d12a95..be641de1 100644
--- a/internal/flowcontrol/interface.go
+++ b/internal/flowcontrol/interface.go
@@ -1,6 +1,10 @@
 package flowcontrol
 
-import "github.com/quic-go/quic-go/internal/protocol"
+import (
+	"time"
+
+	"github.com/quic-go/quic-go/internal/protocol"
+)
 
 type flowController interface {
 	// for sending
@@ -8,7 +12,7 @@ type flowController interface {
 	UpdateSendWindow(protocol.ByteCount) (updated bool)
 	AddBytesSent(protocol.ByteCount)
 	// for receiving
-	GetWindowUpdate() protocol.ByteCount // returns 0 if no update is necessary
+	GetWindowUpdate(time.Time) protocol.ByteCount // returns 0 if no update is necessary
 }
 
 // A StreamFlowController is a flow controller for a QUIC stream.
@@ -18,7 +22,7 @@ type StreamFlowController interface {
 	// UpdateHighestReceived is called when a new highest offset is received
 	// final has to be to true if this is the final offset of the stream,
 	// as contained in a STREAM frame with FIN bit, and the RESET_STREAM frame
-	UpdateHighestReceived(offset protocol.ByteCount, final bool) error
+	UpdateHighestReceived(offset protocol.ByteCount, final bool, now time.Time) error
 	// Abandon is called when reading from the stream is aborted early,
 	// and there won't be any further calls to AddBytesRead.
 	Abandon()
@@ -37,7 +41,7 @@ type connectionFlowControllerI interface {
 	ConnectionFlowController
 	// The following two methods are not supposed to be called from outside this packet, but are needed internally
 	// for sending
-	EnsureMinimumWindowSize(protocol.ByteCount)
+	EnsureMinimumWindowSize(protocol.ByteCount, time.Time)
 	// for receiving
-	IncrementHighestReceived(protocol.ByteCount) error
+	IncrementHighestReceived(protocol.ByteCount, time.Time) error
 }
diff --git a/internal/flowcontrol/stream_flow_controller.go b/internal/flowcontrol/stream_flow_controller.go
index 2d58351c..e12f1f46 100644
--- a/internal/flowcontrol/stream_flow_controller.go
+++ b/internal/flowcontrol/stream_flow_controller.go
@@ -2,6 +2,7 @@ package flowcontrol
 
 import (
 	"fmt"
+	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
@@ -45,7 +46,7 @@ func NewStreamFlowController(
 }
 
 // UpdateHighestReceived updates the highestReceived value, if the offset is higher.
-func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount, final bool) error {
+func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount, final bool, now time.Time) error {
 	// If the final offset for this stream is already known, check for consistency.
 	if c.receivedFinalOffset {
 		// If we receive another final offset, check that it's the same.
@@ -70,9 +71,8 @@ func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount,
 	if offset == c.highestReceived {
 		return nil
 	}
-	// A higher offset was received before.
-	// This can happen due to reordering.
-	if offset <= c.highestReceived {
+	// A higher offset was received before. This can happen due to reordering.
+	if offset < c.highestReceived {
 		if final {
 			return &qerr.TransportError{
 				ErrorCode:    qerr.FinalSizeError,
@@ -82,15 +82,20 @@ func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount,
 		return nil
 	}
 
+	// If this is the first frame received for this stream, start flow-control auto-tuning.
+	if c.highestReceived == 0 {
+		c.startNewAutoTuningEpoch(now)
+	}
 	increment := offset - c.highestReceived
 	c.highestReceived = offset
+
 	if c.checkFlowControlViolation() {
 		return &qerr.TransportError{
 			ErrorCode:    qerr.FlowControlError,
 			ErrorMessage: fmt.Sprintf("received %d bytes on stream %d, allowed %d bytes", offset, c.streamID, c.receiveWindow),
 		}
 	}
-	return c.connection.IncrementHighestReceived(increment)
+	return c.connection.IncrementHighestReceived(increment, now)
 }
 
 func (c *streamFlowController) AddBytesRead(n protocol.ByteCount) (shouldQueueWindowUpdate bool) {
@@ -118,7 +123,7 @@ func (c *streamFlowController) AddBytesSent(n protocol.ByteCount) {
 }
 
 func (c *streamFlowController) SendWindowSize() protocol.ByteCount {
-	return min(c.baseFlowController.sendWindowSize(), c.connection.SendWindowSize())
+	return min(c.baseFlowController.SendWindowSize(), c.connection.SendWindowSize())
 }
 
 func (c *streamFlowController) IsNewlyBlocked() bool {
@@ -130,20 +135,20 @@ func (c *streamFlowController) shouldQueueWindowUpdate() bool {
 	return !c.receivedFinalOffset && c.hasWindowUpdate()
 }
 
-func (c *streamFlowController) GetWindowUpdate() protocol.ByteCount {
+func (c *streamFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount {
 	// If we already received the final offset for this stream, the peer won't need any additional flow control credit.
 	if c.receivedFinalOffset {
 		return 0
 	}
 
-	// Don't use defer for unlocking the mutex here, GetWindowUpdate() is called frequently and defer shows up in the profiler
 	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate()
+	offset := c.baseFlowController.getWindowUpdate(now)
 	if c.receiveWindowSize > oldWindowSize { // auto-tuning enlarged the window size
-		c.logger.Debugf("Increasing receive flow control window for stream %d to %d kB", c.streamID, c.receiveWindowSize/(1<<10))
-		c.connection.EnsureMinimumWindowSize(protocol.ByteCount(float64(c.receiveWindowSize) * protocol.ConnectionFlowControlMultiplier))
+		c.logger.Debugf("Increasing receive flow control window for stream %d to %d", c.streamID, c.receiveWindowSize)
+		c.connection.EnsureMinimumWindowSize(protocol.ByteCount(float64(c.receiveWindowSize)*protocol.ConnectionFlowControlMultiplier), now)
 	}
-	c.mutex.Unlock()
 	return offset
 }
diff --git a/internal/mocks/connection_flow_controller.go b/internal/mocks/connection_flow_controller.go
index cfb7b6be..e16c26d5 100644
--- a/internal/mocks/connection_flow_controller.go
+++ b/internal/mocks/connection_flow_controller.go
@@ -11,6 +11,7 @@ package mocks
 
 import (
 	reflect "reflect"
+	time "time"
 
 	protocol "github.com/quic-go/quic-go/internal/protocol"
 	gomock "go.uber.org/mock/gomock"
@@ -112,17 +113,17 @@ func (c *MockConnectionFlowControllerAddBytesSentCall) DoAndReturn(f func(protoc
 }
 
 // GetWindowUpdate mocks base method.
-func (m *MockConnectionFlowController) GetWindowUpdate() protocol.ByteCount {
+func (m *MockConnectionFlowController) GetWindowUpdate(arg0 time.Time) protocol.ByteCount {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetWindowUpdate")
+	ret := m.ctrl.Call(m, "GetWindowUpdate", arg0)
 	ret0, _ := ret[0].(protocol.ByteCount)
 	return ret0
 }
 
 // GetWindowUpdate indicates an expected call of GetWindowUpdate.
-func (mr *MockConnectionFlowControllerMockRecorder) GetWindowUpdate() *MockConnectionFlowControllerGetWindowUpdateCall {
+func (mr *MockConnectionFlowControllerMockRecorder) GetWindowUpdate(arg0 any) *MockConnectionFlowControllerGetWindowUpdateCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWindowUpdate", reflect.TypeOf((*MockConnectionFlowController)(nil).GetWindowUpdate))
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWindowUpdate", reflect.TypeOf((*MockConnectionFlowController)(nil).GetWindowUpdate), arg0)
 	return &MockConnectionFlowControllerGetWindowUpdateCall{Call: call}
 }
 
@@ -138,13 +139,13 @@ func (c *MockConnectionFlowControllerGetWindowUpdateCall) Return(arg0 protocol.B
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockConnectionFlowControllerGetWindowUpdateCall) Do(f func() protocol.ByteCount) *MockConnectionFlowControllerGetWindowUpdateCall {
+func (c *MockConnectionFlowControllerGetWindowUpdateCall) Do(f func(time.Time) protocol.ByteCount) *MockConnectionFlowControllerGetWindowUpdateCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockConnectionFlowControllerGetWindowUpdateCall) DoAndReturn(f func() protocol.ByteCount) *MockConnectionFlowControllerGetWindowUpdateCall {
+func (c *MockConnectionFlowControllerGetWindowUpdateCall) DoAndReturn(f func(time.Time) protocol.ByteCount) *MockConnectionFlowControllerGetWindowUpdateCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/internal/mocks/stream_flow_controller.go b/internal/mocks/stream_flow_controller.go
index b5ca05c1..8ddc5d11 100644
--- a/internal/mocks/stream_flow_controller.go
+++ b/internal/mocks/stream_flow_controller.go
@@ -11,6 +11,7 @@ package mocks
 
 import (
 	reflect "reflect"
+	time "time"
 
 	protocol "github.com/quic-go/quic-go/internal/protocol"
 	gomock "go.uber.org/mock/gomock"
@@ -150,17 +151,17 @@ func (c *MockStreamFlowControllerAddBytesSentCall) DoAndReturn(f func(protocol.B
 }
 
 // GetWindowUpdate mocks base method.
-func (m *MockStreamFlowController) GetWindowUpdate() protocol.ByteCount {
+func (m *MockStreamFlowController) GetWindowUpdate(arg0 time.Time) protocol.ByteCount {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetWindowUpdate")
+	ret := m.ctrl.Call(m, "GetWindowUpdate", arg0)
 	ret0, _ := ret[0].(protocol.ByteCount)
 	return ret0
 }
 
 // GetWindowUpdate indicates an expected call of GetWindowUpdate.
-func (mr *MockStreamFlowControllerMockRecorder) GetWindowUpdate() *MockStreamFlowControllerGetWindowUpdateCall {
+func (mr *MockStreamFlowControllerMockRecorder) GetWindowUpdate(arg0 any) *MockStreamFlowControllerGetWindowUpdateCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWindowUpdate", reflect.TypeOf((*MockStreamFlowController)(nil).GetWindowUpdate))
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWindowUpdate", reflect.TypeOf((*MockStreamFlowController)(nil).GetWindowUpdate), arg0)
 	return &MockStreamFlowControllerGetWindowUpdateCall{Call: call}
 }
 
@@ -176,13 +177,13 @@ func (c *MockStreamFlowControllerGetWindowUpdateCall) Return(arg0 protocol.ByteC
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamFlowControllerGetWindowUpdateCall) Do(f func() protocol.ByteCount) *MockStreamFlowControllerGetWindowUpdateCall {
+func (c *MockStreamFlowControllerGetWindowUpdateCall) Do(f func(time.Time) protocol.ByteCount) *MockStreamFlowControllerGetWindowUpdateCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamFlowControllerGetWindowUpdateCall) DoAndReturn(f func() protocol.ByteCount) *MockStreamFlowControllerGetWindowUpdateCall {
+func (c *MockStreamFlowControllerGetWindowUpdateCall) DoAndReturn(f func(time.Time) protocol.ByteCount) *MockStreamFlowControllerGetWindowUpdateCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -264,17 +265,17 @@ func (c *MockStreamFlowControllerSendWindowSizeCall) DoAndReturn(f func() protoc
 }
 
 // UpdateHighestReceived mocks base method.
-func (m *MockStreamFlowController) UpdateHighestReceived(arg0 protocol.ByteCount, arg1 bool) error {
+func (m *MockStreamFlowController) UpdateHighestReceived(arg0 protocol.ByteCount, arg1 bool, arg2 time.Time) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UpdateHighestReceived", arg0, arg1)
+	ret := m.ctrl.Call(m, "UpdateHighestReceived", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // UpdateHighestReceived indicates an expected call of UpdateHighestReceived.
-func (mr *MockStreamFlowControllerMockRecorder) UpdateHighestReceived(arg0, arg1 any) *MockStreamFlowControllerUpdateHighestReceivedCall {
+func (mr *MockStreamFlowControllerMockRecorder) UpdateHighestReceived(arg0, arg1, arg2 any) *MockStreamFlowControllerUpdateHighestReceivedCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateHighestReceived", reflect.TypeOf((*MockStreamFlowController)(nil).UpdateHighestReceived), arg0, arg1)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateHighestReceived", reflect.TypeOf((*MockStreamFlowController)(nil).UpdateHighestReceived), arg0, arg1, arg2)
 	return &MockStreamFlowControllerUpdateHighestReceivedCall{Call: call}
 }
 
@@ -290,13 +291,13 @@ func (c *MockStreamFlowControllerUpdateHighestReceivedCall) Return(arg0 error) *
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamFlowControllerUpdateHighestReceivedCall) Do(f func(protocol.ByteCount, bool) error) *MockStreamFlowControllerUpdateHighestReceivedCall {
+func (c *MockStreamFlowControllerUpdateHighestReceivedCall) Do(f func(protocol.ByteCount, bool, time.Time) error) *MockStreamFlowControllerUpdateHighestReceivedCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamFlowControllerUpdateHighestReceivedCall) DoAndReturn(f func(protocol.ByteCount, bool) error) *MockStreamFlowControllerUpdateHighestReceivedCall {
+func (c *MockStreamFlowControllerUpdateHighestReceivedCall) DoAndReturn(f func(protocol.ByteCount, bool, time.Time) error) *MockStreamFlowControllerUpdateHighestReceivedCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/logging/connection_tracer.go b/logging/connection_tracer.go
index 96bf4617..f218e046 100644
--- a/logging/connection_tracer.go
+++ b/logging/connection_tracer.go
@@ -5,34 +5,36 @@ import (
 	"time"
 )
 
+//go:generate go run generate_multiplexer.go ConnectionTracer connection_tracer.go multiplexer.tmpl connection_tracer_multiplexer.go
+
 // A ConnectionTracer records events.
 type ConnectionTracer struct {
 	StartedConnection                func(local, remote net.Addr, srcConnID, destConnID ConnectionID)
 	NegotiatedVersion                func(chosen Version, clientVersions, serverVersions []Version)
-	ClosedConnection                 func(error)
-	SentTransportParameters          func(*TransportParameters)
-	ReceivedTransportParameters      func(*TransportParameters)
+	ClosedConnection                 func(err error)
+	SentTransportParameters          func(parameters *TransportParameters)
+	ReceivedTransportParameters      func(parameters *TransportParameters)
 	RestoredTransportParameters      func(parameters *TransportParameters) // for 0-RTT
-	SentLongHeaderPacket             func(*ExtendedHeader, ByteCount, ECN, *AckFrame, []Frame)
-	SentShortHeaderPacket            func(*ShortHeader, ByteCount, ECN, *AckFrame, []Frame)
-	ReceivedVersionNegotiationPacket func(dest, src ArbitraryLenConnectionID, _ []Version)
-	ReceivedRetry                    func(*Header)
-	ReceivedLongHeaderPacket         func(*ExtendedHeader, ByteCount, ECN, []Frame)
-	ReceivedShortHeaderPacket        func(*ShortHeader, ByteCount, ECN, []Frame)
-	BufferedPacket                   func(PacketType, ByteCount)
-	DroppedPacket                    func(PacketType, PacketNumber, ByteCount, PacketDropReason)
+	SentLongHeaderPacket             func(hdr *ExtendedHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame)
+	SentShortHeaderPacket            func(hdr *ShortHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame)
+	ReceivedVersionNegotiationPacket func(dest, src ArbitraryLenConnectionID, versions []Version)
+	ReceivedRetry                    func(hdr *Header)
+	ReceivedLongHeaderPacket         func(hdr *ExtendedHeader, size ByteCount, ecn ECN, frames []Frame)
+	ReceivedShortHeaderPacket        func(hdr *ShortHeader, size ByteCount, ecn ECN, frames []Frame)
+	BufferedPacket                   func(packetType PacketType, size ByteCount)
+	DroppedPacket                    func(packetType PacketType, pn PacketNumber, size ByteCount, reason PacketDropReason)
 	UpdatedMetrics                   func(rttStats *RTTStats, cwnd, bytesInFlight ByteCount, packetsInFlight int)
-	AcknowledgedPacket               func(EncryptionLevel, PacketNumber)
-	LostPacket                       func(EncryptionLevel, PacketNumber, PacketLossReason)
+	AcknowledgedPacket               func(encLevel EncryptionLevel, pn PacketNumber)
+	LostPacket                       func(encLevel EncryptionLevel, pn PacketNumber, reason PacketLossReason)
 	UpdatedMTU                       func(mtu ByteCount, done bool)
-	UpdatedCongestionState           func(CongestionState)
+	UpdatedCongestionState           func(state CongestionState)
 	UpdatedPTOCount                  func(value uint32)
-	UpdatedKeyFromTLS                func(EncryptionLevel, Perspective)
+	UpdatedKeyFromTLS                func(encLevel EncryptionLevel, p Perspective)
 	UpdatedKey                       func(keyPhase KeyPhase, remote bool)
-	DroppedEncryptionLevel           func(EncryptionLevel)
+	DroppedEncryptionLevel           func(encLevel EncryptionLevel)
 	DroppedKey                       func(keyPhase KeyPhase)
-	SetLossTimer                     func(TimerType, EncryptionLevel, time.Time)
-	LossTimerExpired                 func(TimerType, EncryptionLevel)
+	SetLossTimer                     func(timerType TimerType, encLevel EncryptionLevel, time time.Time)
+	LossTimerExpired                 func(timerType TimerType, encLevel EncryptionLevel)
 	LossTimerCanceled                func()
 	ECNStateUpdated                  func(state ECNState, trigger ECNStateTrigger)
 	ChoseALPN                        func(protocol string)
@@ -40,232 +42,3 @@ type ConnectionTracer struct {
 	Close func()
 	Debug func(name, msg string)
 }
-
-// NewMultiplexedConnectionTracer creates a new connection tracer that multiplexes events to multiple tracers.
-func NewMultiplexedConnectionTracer(tracers ...*ConnectionTracer) *ConnectionTracer {
-	if len(tracers) == 0 {
-		return nil
-	}
-	if len(tracers) == 1 {
-		return tracers[0]
-	}
-	return &ConnectionTracer{
-		StartedConnection: func(local, remote net.Addr, srcConnID, destConnID ConnectionID) {
-			for _, t := range tracers {
-				if t.StartedConnection != nil {
-					t.StartedConnection(local, remote, srcConnID, destConnID)
-				}
-			}
-		},
-		NegotiatedVersion: func(chosen Version, clientVersions, serverVersions []Version) {
-			for _, t := range tracers {
-				if t.NegotiatedVersion != nil {
-					t.NegotiatedVersion(chosen, clientVersions, serverVersions)
-				}
-			}
-		},
-		ClosedConnection: func(e error) {
-			for _, t := range tracers {
-				if t.ClosedConnection != nil {
-					t.ClosedConnection(e)
-				}
-			}
-		},
-		SentTransportParameters: func(tp *TransportParameters) {
-			for _, t := range tracers {
-				if t.SentTransportParameters != nil {
-					t.SentTransportParameters(tp)
-				}
-			}
-		},
-		ReceivedTransportParameters: func(tp *TransportParameters) {
-			for _, t := range tracers {
-				if t.ReceivedTransportParameters != nil {
-					t.ReceivedTransportParameters(tp)
-				}
-			}
-		},
-		RestoredTransportParameters: func(tp *TransportParameters) {
-			for _, t := range tracers {
-				if t.RestoredTransportParameters != nil {
-					t.RestoredTransportParameters(tp)
-				}
-			}
-		},
-		SentLongHeaderPacket: func(hdr *ExtendedHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame) {
-			for _, t := range tracers {
-				if t.SentLongHeaderPacket != nil {
-					t.SentLongHeaderPacket(hdr, size, ecn, ack, frames)
-				}
-			}
-		},
-		SentShortHeaderPacket: func(hdr *ShortHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame) {
-			for _, t := range tracers {
-				if t.SentShortHeaderPacket != nil {
-					t.SentShortHeaderPacket(hdr, size, ecn, ack, frames)
-				}
-			}
-		},
-		ReceivedVersionNegotiationPacket: func(dest, src ArbitraryLenConnectionID, versions []Version) {
-			for _, t := range tracers {
-				if t.ReceivedVersionNegotiationPacket != nil {
-					t.ReceivedVersionNegotiationPacket(dest, src, versions)
-				}
-			}
-		},
-		ReceivedRetry: func(hdr *Header) {
-			for _, t := range tracers {
-				if t.ReceivedRetry != nil {
-					t.ReceivedRetry(hdr)
-				}
-			}
-		},
-		ReceivedLongHeaderPacket: func(hdr *ExtendedHeader, size ByteCount, ecn ECN, frames []Frame) {
-			for _, t := range tracers {
-				if t.ReceivedLongHeaderPacket != nil {
-					t.ReceivedLongHeaderPacket(hdr, size, ecn, frames)
-				}
-			}
-		},
-		ReceivedShortHeaderPacket: func(hdr *ShortHeader, size ByteCount, ecn ECN, frames []Frame) {
-			for _, t := range tracers {
-				if t.ReceivedShortHeaderPacket != nil {
-					t.ReceivedShortHeaderPacket(hdr, size, ecn, frames)
-				}
-			}
-		},
-		BufferedPacket: func(typ PacketType, size ByteCount) {
-			for _, t := range tracers {
-				if t.BufferedPacket != nil {
-					t.BufferedPacket(typ, size)
-				}
-			}
-		},
-		DroppedPacket: func(typ PacketType, pn PacketNumber, size ByteCount, reason PacketDropReason) {
-			for _, t := range tracers {
-				if t.DroppedPacket != nil {
-					t.DroppedPacket(typ, pn, size, reason)
-				}
-			}
-		},
-		UpdatedMetrics: func(rttStats *RTTStats, cwnd, bytesInFlight ByteCount, packetsInFlight int) {
-			for _, t := range tracers {
-				if t.UpdatedMetrics != nil {
-					t.UpdatedMetrics(rttStats, cwnd, bytesInFlight, packetsInFlight)
-				}
-			}
-		},
-		AcknowledgedPacket: func(encLevel EncryptionLevel, pn PacketNumber) {
-			for _, t := range tracers {
-				if t.AcknowledgedPacket != nil {
-					t.AcknowledgedPacket(encLevel, pn)
-				}
-			}
-		},
-		LostPacket: func(encLevel EncryptionLevel, pn PacketNumber, reason PacketLossReason) {
-			for _, t := range tracers {
-				if t.LostPacket != nil {
-					t.LostPacket(encLevel, pn, reason)
-				}
-			}
-		},
-		UpdatedMTU: func(mtu ByteCount, done bool) {
-			for _, t := range tracers {
-				if t.UpdatedMTU != nil {
-					t.UpdatedMTU(mtu, done)
-				}
-			}
-		},
-		UpdatedCongestionState: func(state CongestionState) {
-			for _, t := range tracers {
-				if t.UpdatedCongestionState != nil {
-					t.UpdatedCongestionState(state)
-				}
-			}
-		},
-		UpdatedPTOCount: func(value uint32) {
-			for _, t := range tracers {
-				if t.UpdatedPTOCount != nil {
-					t.UpdatedPTOCount(value)
-				}
-			}
-		},
-		UpdatedKeyFromTLS: func(encLevel EncryptionLevel, perspective Perspective) {
-			for _, t := range tracers {
-				if t.UpdatedKeyFromTLS != nil {
-					t.UpdatedKeyFromTLS(encLevel, perspective)
-				}
-			}
-		},
-		UpdatedKey: func(generation KeyPhase, remote bool) {
-			for _, t := range tracers {
-				if t.UpdatedKey != nil {
-					t.UpdatedKey(generation, remote)
-				}
-			}
-		},
-		DroppedEncryptionLevel: func(encLevel EncryptionLevel) {
-			for _, t := range tracers {
-				if t.DroppedEncryptionLevel != nil {
-					t.DroppedEncryptionLevel(encLevel)
-				}
-			}
-		},
-		DroppedKey: func(generation KeyPhase) {
-			for _, t := range tracers {
-				if t.DroppedKey != nil {
-					t.DroppedKey(generation)
-				}
-			}
-		},
-		SetLossTimer: func(typ TimerType, encLevel EncryptionLevel, exp time.Time) {
-			for _, t := range tracers {
-				if t.SetLossTimer != nil {
-					t.SetLossTimer(typ, encLevel, exp)
-				}
-			}
-		},
-		LossTimerExpired: func(typ TimerType, encLevel EncryptionLevel) {
-			for _, t := range tracers {
-				if t.LossTimerExpired != nil {
-					t.LossTimerExpired(typ, encLevel)
-				}
-			}
-		},
-		LossTimerCanceled: func() {
-			for _, t := range tracers {
-				if t.LossTimerCanceled != nil {
-					t.LossTimerCanceled()
-				}
-			}
-		},
-		ECNStateUpdated: func(state ECNState, trigger ECNStateTrigger) {
-			for _, t := range tracers {
-				if t.ECNStateUpdated != nil {
-					t.ECNStateUpdated(state, trigger)
-				}
-			}
-		},
-		ChoseALPN: func(protocol string) {
-			for _, t := range tracers {
-				if t.ChoseALPN != nil {
-					t.ChoseALPN(protocol)
-				}
-			}
-		},
-		Close: func() {
-			for _, t := range tracers {
-				if t.Close != nil {
-					t.Close()
-				}
-			}
-		},
-		Debug: func(name, msg string) {
-			for _, t := range tracers {
-				if t.Debug != nil {
-					t.Debug(name, msg)
-				}
-			}
-		},
-	}
-}
diff --git a/logging/connection_tracer_multiplexer.go b/logging/connection_tracer_multiplexer.go
new file mode 100644
index 00000000..3a87058c
--- /dev/null
+++ b/logging/connection_tracer_multiplexer.go
@@ -0,0 +1,236 @@
+// Code generated by generate_multiplexer.go; DO NOT EDIT.
+
+package logging
+
+import (
+	"net"
+	"time"
+)
+
+func NewMultiplexedConnectionTracer(tracers ...*ConnectionTracer) *ConnectionTracer {
+	if len(tracers) == 0 {
+		return nil
+	}
+	if len(tracers) == 1 {
+		return tracers[0]
+	}
+	return &ConnectionTracer{
+		StartedConnection: func(local net.Addr, remote net.Addr, srcConnID ConnectionID, destConnID ConnectionID) {
+			for _, t := range tracers {
+				if t.StartedConnection != nil {
+					t.StartedConnection(local, remote, srcConnID, destConnID)
+				}
+			}
+		},
+		NegotiatedVersion: func(chosen Version, clientVersions []Version, serverVersions []Version) {
+			for _, t := range tracers {
+				if t.NegotiatedVersion != nil {
+					t.NegotiatedVersion(chosen, clientVersions, serverVersions)
+				}
+			}
+		},
+		ClosedConnection: func(err error) {
+			for _, t := range tracers {
+				if t.ClosedConnection != nil {
+					t.ClosedConnection(err)
+				}
+			}
+		},
+		SentTransportParameters: func(parameters *TransportParameters) {
+			for _, t := range tracers {
+				if t.SentTransportParameters != nil {
+					t.SentTransportParameters(parameters)
+				}
+			}
+		},
+		ReceivedTransportParameters: func(parameters *TransportParameters) {
+			for _, t := range tracers {
+				if t.ReceivedTransportParameters != nil {
+					t.ReceivedTransportParameters(parameters)
+				}
+			}
+		},
+		RestoredTransportParameters: func(parameters *TransportParameters) {
+			for _, t := range tracers {
+				if t.RestoredTransportParameters != nil {
+					t.RestoredTransportParameters(parameters)
+				}
+			}
+		},
+		SentLongHeaderPacket: func(hdr *ExtendedHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame) {
+			for _, t := range tracers {
+				if t.SentLongHeaderPacket != nil {
+					t.SentLongHeaderPacket(hdr, size, ecn, ack, frames)
+				}
+			}
+		},
+		SentShortHeaderPacket: func(hdr *ShortHeader, size ByteCount, ecn ECN, ack *AckFrame, frames []Frame) {
+			for _, t := range tracers {
+				if t.SentShortHeaderPacket != nil {
+					t.SentShortHeaderPacket(hdr, size, ecn, ack, frames)
+				}
+			}
+		},
+		ReceivedVersionNegotiationPacket: func(dest ArbitraryLenConnectionID, src ArbitraryLenConnectionID, versions []Version) {
+			for _, t := range tracers {
+				if t.ReceivedVersionNegotiationPacket != nil {
+					t.ReceivedVersionNegotiationPacket(dest, src, versions)
+				}
+			}
+		},
+		ReceivedRetry: func(hdr *Header) {
+			for _, t := range tracers {
+				if t.ReceivedRetry != nil {
+					t.ReceivedRetry(hdr)
+				}
+			}
+		},
+		ReceivedLongHeaderPacket: func(hdr *ExtendedHeader, size ByteCount, ecn ECN, frames []Frame) {
+			for _, t := range tracers {
+				if t.ReceivedLongHeaderPacket != nil {
+					t.ReceivedLongHeaderPacket(hdr, size, ecn, frames)
+				}
+			}
+		},
+		ReceivedShortHeaderPacket: func(hdr *ShortHeader, size ByteCount, ecn ECN, frames []Frame) {
+			for _, t := range tracers {
+				if t.ReceivedShortHeaderPacket != nil {
+					t.ReceivedShortHeaderPacket(hdr, size, ecn, frames)
+				}
+			}
+		},
+		BufferedPacket: func(packetType PacketType, size ByteCount) {
+			for _, t := range tracers {
+				if t.BufferedPacket != nil {
+					t.BufferedPacket(packetType, size)
+				}
+			}
+		},
+		DroppedPacket: func(packetType PacketType, pn PacketNumber, size ByteCount, reason PacketDropReason) {
+			for _, t := range tracers {
+				if t.DroppedPacket != nil {
+					t.DroppedPacket(packetType, pn, size, reason)
+				}
+			}
+		},
+		UpdatedMetrics: func(rttStats *RTTStats, cwnd ByteCount, bytesInFlight ByteCount, packetsInFlight int) {
+			for _, t := range tracers {
+				if t.UpdatedMetrics != nil {
+					t.UpdatedMetrics(rttStats, cwnd, bytesInFlight, packetsInFlight)
+				}
+			}
+		},
+		AcknowledgedPacket: func(encLevel EncryptionLevel, pn PacketNumber) {
+			for _, t := range tracers {
+				if t.AcknowledgedPacket != nil {
+					t.AcknowledgedPacket(encLevel, pn)
+				}
+			}
+		},
+		LostPacket: func(encLevel EncryptionLevel, pn PacketNumber, reason PacketLossReason) {
+			for _, t := range tracers {
+				if t.LostPacket != nil {
+					t.LostPacket(encLevel, pn, reason)
+				}
+			}
+		},
+		UpdatedMTU: func(mtu ByteCount, done bool) {
+			for _, t := range tracers {
+				if t.UpdatedMTU != nil {
+					t.UpdatedMTU(mtu, done)
+				}
+			}
+		},
+		UpdatedCongestionState: func(state CongestionState) {
+			for _, t := range tracers {
+				if t.UpdatedCongestionState != nil {
+					t.UpdatedCongestionState(state)
+				}
+			}
+		},
+		UpdatedPTOCount: func(value uint32) {
+			for _, t := range tracers {
+				if t.UpdatedPTOCount != nil {
+					t.UpdatedPTOCount(value)
+				}
+			}
+		},
+		UpdatedKeyFromTLS: func(encLevel EncryptionLevel, p Perspective) {
+			for _, t := range tracers {
+				if t.UpdatedKeyFromTLS != nil {
+					t.UpdatedKeyFromTLS(encLevel, p)
+				}
+			}
+		},
+		UpdatedKey: func(keyPhase KeyPhase, remote bool) {
+			for _, t := range tracers {
+				if t.UpdatedKey != nil {
+					t.UpdatedKey(keyPhase, remote)
+				}
+			}
+		},
+		DroppedEncryptionLevel: func(encLevel EncryptionLevel) {
+			for _, t := range tracers {
+				if t.DroppedEncryptionLevel != nil {
+					t.DroppedEncryptionLevel(encLevel)
+				}
+			}
+		},
+		DroppedKey: func(keyPhase KeyPhase) {
+			for _, t := range tracers {
+				if t.DroppedKey != nil {
+					t.DroppedKey(keyPhase)
+				}
+			}
+		},
+		SetLossTimer: func(timerType TimerType, encLevel EncryptionLevel, time time.Time) {
+			for _, t := range tracers {
+				if t.SetLossTimer != nil {
+					t.SetLossTimer(timerType, encLevel, time)
+				}
+			}
+		},
+		LossTimerExpired: func(timerType TimerType, encLevel EncryptionLevel) {
+			for _, t := range tracers {
+				if t.LossTimerExpired != nil {
+					t.LossTimerExpired(timerType, encLevel)
+				}
+			}
+		},
+		LossTimerCanceled: func() {
+			for _, t := range tracers {
+				if t.LossTimerCanceled != nil {
+					t.LossTimerCanceled()
+				}
+			}
+		},
+		ECNStateUpdated: func(state ECNState, trigger ECNStateTrigger) {
+			for _, t := range tracers {
+				if t.ECNStateUpdated != nil {
+					t.ECNStateUpdated(state, trigger)
+				}
+			}
+		},
+		ChoseALPN: func(protocol string) {
+			for _, t := range tracers {
+				if t.ChoseALPN != nil {
+					t.ChoseALPN(protocol)
+				}
+			}
+		},
+		Close: func() {
+			for _, t := range tracers {
+				if t.Close != nil {
+					t.Close()
+				}
+			}
+		},
+		Debug: func(name string, msg string) {
+			for _, t := range tracers {
+				if t.Debug != nil {
+					t.Debug(name, msg)
+				}
+			}
+		},
+	}
+}
diff --git a/logging/generate_multiplexer.go b/logging/generate_multiplexer.go
new file mode 100644
index 00000000..c152b846
--- /dev/null
+++ b/logging/generate_multiplexer.go
@@ -0,0 +1,161 @@
+//go:build generate
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/printer"
+	"go/token"
+	"log"
+	"os"
+	"strings"
+	"text/template"
+
+	"golang.org/x/tools/imports"
+)
+
+func main() {
+	if len(os.Args) != 5 {
+		log.Fatalf("Usage: %s <struct_name> <input_file> <template_file> <output_file>", os.Args[0])
+	}
+
+	structName := os.Args[1]
+	inputFile := os.Args[2]
+	templateFile := os.Args[3]
+	outputFile := os.Args[4]
+
+	fset := token.NewFileSet()
+
+	// Parse the input file containing the struct type
+	file, err := parser.ParseFile(fset, inputFile, nil, parser.AllErrors)
+	if err != nil {
+		log.Fatalf("Failed to parse file: %v", err)
+	}
+
+	var fields []*ast.Field
+
+	// Find the specified struct type in the AST
+	for _, decl := range file.Decls {
+		genDecl, ok := decl.(*ast.GenDecl)
+		if !ok || genDecl.Tok != token.TYPE {
+			continue
+		}
+		for _, spec := range genDecl.Specs {
+			typeSpec, ok := spec.(*ast.TypeSpec)
+			if !ok || typeSpec.Name.Name != structName {
+				continue
+			}
+			structType, ok := typeSpec.Type.(*ast.StructType)
+			if !ok {
+				log.Fatalf("%s is not a struct", structName)
+			}
+			fields = structType.Fields.List
+			break
+		}
+	}
+
+	if fields == nil {
+		log.Fatalf("Could not find %s type", structName)
+	}
+
+	// Prepare data for the template
+	type FieldData struct {
+		Name        string
+		Params      string
+		Args        string
+		HasParams   bool
+		ReturnTypes string
+		HasReturn   bool
+	}
+
+	var fieldDataList []FieldData
+
+	for _, field := range fields {
+		funcType, ok := field.Type.(*ast.FuncType)
+		if !ok {
+			continue
+		}
+		for _, name := range field.Names {
+			fieldData := FieldData{Name: name.Name}
+
+			// extract parameters
+			var params []string
+			var args []string
+			if funcType.Params != nil {
+				for i, param := range funcType.Params.List {
+					// We intentionally reject unnamed (and, further down, "_") function parameters.
+					// We could auto-generate parameter names,
+					// but having meaningful variable names will be more helpful for the user.
+					if len(param.Names) == 0 {
+						log.Fatalf("encountered unnamed parameter at position %d in function %s", i, fieldData.Name)
+					}
+					var buf bytes.Buffer
+					printer.Fprint(&buf, fset, param.Type)
+					paramType := buf.String()
+					for _, paramName := range param.Names {
+						if paramName.Name == "_" {
+							log.Fatalf("encountered underscore parameter at position %d in function %s", i, fieldData.Name)
+						}
+						params = append(params, fmt.Sprintf("%s %s", paramName.Name, paramType))
+						args = append(args, paramName.Name)
+					}
+				}
+			}
+			fieldData.Params = strings.Join(params, ", ")
+			fieldData.Args = strings.Join(args, ", ")
+			fieldData.HasParams = len(params) > 0
+
+			// extract return types
+			if funcType.Results != nil && len(funcType.Results.List) > 0 {
+				fieldData.HasReturn = true
+				var returns []string
+				for _, result := range funcType.Results.List {
+					var buf bytes.Buffer
+					printer.Fprint(&buf, fset, result.Type)
+					returns = append(returns, buf.String())
+				}
+				if len(returns) == 1 {
+					fieldData.ReturnTypes = fmt.Sprintf(" %s", returns[0])
+				} else {
+					fieldData.ReturnTypes = fmt.Sprintf(" (%s)", strings.Join(returns, ", "))
+				}
+			}
+
+			fieldDataList = append(fieldDataList, fieldData)
+		}
+	}
+
+	// Read the template from file
+	templateContent, err := os.ReadFile(templateFile)
+	if err != nil {
+		log.Fatalf("Failed to read template file: %v", err)
+	}
+
+	// Generate the code using the template
+	tmpl, err := template.New("multiplexer").Funcs(template.FuncMap{"join": strings.Join}).Parse(string(templateContent))
+	if err != nil {
+		log.Fatalf("Failed to parse template: %v", err)
+	}
+
+	var generatedCode bytes.Buffer
+	generatedCode.WriteString("// Code generated by generate_multiplexer.go; DO NOT EDIT.\n\n")
+	if err = tmpl.Execute(&generatedCode, map[string]interface{}{
+		"Fields":     fieldDataList,
+		"StructName": structName,
+	}); err != nil {
+		log.Fatalf("Failed to execute template: %v", err)
+	}
+
+	// Format the generated code and add imports
+	formattedCode, err := imports.Process(outputFile, generatedCode.Bytes(), nil)
+	if err != nil {
+		log.Fatalf("Failed to process imports: %v", err)
+	}
+
+	if err := os.WriteFile(outputFile, formattedCode, 0o644); err != nil {
+		log.Fatalf("Failed to write output file: %v", err)
+	}
+}
diff --git a/logging/multiplexer.tmpl b/logging/multiplexer.tmpl
new file mode 100644
index 00000000..9ba52e0f
--- /dev/null
+++ b/logging/multiplexer.tmpl
@@ -0,0 +1,21 @@
+package logging
+
+func NewMultiplexed{{ .StructName }} (tracers ...*{{ .StructName }}) *{{ .StructName }} {
+    if len(tracers) == 0 {
+        return nil
+    }
+    if len(tracers) == 1 {
+        return tracers[0]
+    }
+    return &{{ .StructName }}{
+        {{- range .Fields }}
+        {{ .Name }}: func({{ .Params }}){{ .ReturnTypes }} {
+            for _, t := range tracers {
+                if t.{{ .Name }} != nil {
+                    t.{{ .Name }}({{ .Args }})
+                }
+            }
+        },
+        {{- end }}
+    }
+}
diff --git a/logging/tracer.go b/logging/tracer.go
index 625a809e..4fe01462 100644
--- a/logging/tracer.go
+++ b/logging/tracer.go
@@ -2,58 +2,13 @@ package logging
 
 import "net"
 
+//go:generate go run generate_multiplexer.go Tracer tracer.go multiplexer.tmpl tracer_multiplexer.go
+
 // A Tracer traces events.
 type Tracer struct {
-	SentPacket                   func(net.Addr, *Header, ByteCount, []Frame)
-	SentVersionNegotiationPacket func(_ net.Addr, dest, src ArbitraryLenConnectionID, _ []Version)
-	DroppedPacket                func(net.Addr, PacketType, ByteCount, PacketDropReason)
+	SentPacket                   func(dest net.Addr, hdr *Header, size ByteCount, frames []Frame)
+	SentVersionNegotiationPacket func(dest net.Addr, destConnID, srcConnID ArbitraryLenConnectionID, versions []Version)
+	DroppedPacket                func(addr net.Addr, packetType PacketType, size ByteCount, reason PacketDropReason)
 	Debug                        func(name, msg string)
 	Close                        func()
 }
-
-// NewMultiplexedTracer creates a new tracer that multiplexes events to multiple tracers.
-func NewMultiplexedTracer(tracers ...*Tracer) *Tracer {
-	if len(tracers) == 0 {
-		return nil
-	}
-	if len(tracers) == 1 {
-		return tracers[0]
-	}
-	return &Tracer{
-		SentPacket: func(remote net.Addr, hdr *Header, size ByteCount, frames []Frame) {
-			for _, t := range tracers {
-				if t.SentPacket != nil {
-					t.SentPacket(remote, hdr, size, frames)
-				}
-			}
-		},
-		SentVersionNegotiationPacket: func(remote net.Addr, dest, src ArbitraryLenConnectionID, versions []Version) {
-			for _, t := range tracers {
-				if t.SentVersionNegotiationPacket != nil {
-					t.SentVersionNegotiationPacket(remote, dest, src, versions)
-				}
-			}
-		},
-		DroppedPacket: func(remote net.Addr, typ PacketType, size ByteCount, reason PacketDropReason) {
-			for _, t := range tracers {
-				if t.DroppedPacket != nil {
-					t.DroppedPacket(remote, typ, size, reason)
-				}
-			}
-		},
-		Debug: func(name, msg string) {
-			for _, t := range tracers {
-				if t.Debug != nil {
-					t.Debug(name, msg)
-				}
-			}
-		},
-		Close: func() {
-			for _, t := range tracers {
-				if t.Close != nil {
-					t.Close()
-				}
-			}
-		},
-	}
-}
diff --git a/logging/tracer_multiplexer.go b/logging/tracer_multiplexer.go
new file mode 100644
index 00000000..f0878cfe
--- /dev/null
+++ b/logging/tracer_multiplexer.go
@@ -0,0 +1,51 @@
+// Code generated by generate_multiplexer.go; DO NOT EDIT.
+
+package logging
+
+import "net"
+
+func NewMultiplexedTracer(tracers ...*Tracer) *Tracer {
+	if len(tracers) == 0 {
+		return nil
+	}
+	if len(tracers) == 1 {
+		return tracers[0]
+	}
+	return &Tracer{
+		SentPacket: func(dest net.Addr, hdr *Header, size ByteCount, frames []Frame) {
+			for _, t := range tracers {
+				if t.SentPacket != nil {
+					t.SentPacket(dest, hdr, size, frames)
+				}
+			}
+		},
+		SentVersionNegotiationPacket: func(dest net.Addr, destConnID ArbitraryLenConnectionID, srcConnID ArbitraryLenConnectionID, versions []Version) {
+			for _, t := range tracers {
+				if t.SentVersionNegotiationPacket != nil {
+					t.SentVersionNegotiationPacket(dest, destConnID, srcConnID, versions)
+				}
+			}
+		},
+		DroppedPacket: func(addr net.Addr, packetType PacketType, size ByteCount, reason PacketDropReason) {
+			for _, t := range tracers {
+				if t.DroppedPacket != nil {
+					t.DroppedPacket(addr, packetType, size, reason)
+				}
+			}
+		},
+		Debug: func(name string, msg string) {
+			for _, t := range tracers {
+				if t.Debug != nil {
+					t.Debug(name, msg)
+				}
+			}
+		},
+		Close: func() {
+			for _, t := range tracers {
+				if t.Close != nil {
+					t.Close()
+				}
+			}
+		},
+	}
+}
diff --git a/packet_packer.go b/packet_packer.go
index 8b8a03d4..388a2083 100644
--- a/packet_packer.go
+++ b/packet_packer.go
@@ -5,6 +5,7 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
+	"time"
 
 	"golang.org/x/exp/rand"
 
@@ -18,10 +19,10 @@ import (
 var errNothingToPack = errors.New("nothing to pack")
 
 type packer interface {
-	PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.ByteCount, v protocol.Version) (*coalescedPacket, error)
-	PackAckOnlyPacket(maxPacketSize protocol.ByteCount, v protocol.Version) (shortHeaderPacket, *packetBuffer, error)
-	AppendPacket(buf *packetBuffer, maxPacketSize protocol.ByteCount, v protocol.Version) (shortHeaderPacket, error)
-	MaybePackProbePacket(protocol.EncryptionLevel, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)
+	PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.ByteCount, now time.Time, v protocol.Version) (*coalescedPacket, error)
+	PackAckOnlyPacket(maxPacketSize protocol.ByteCount, now time.Time, v protocol.Version) (shortHeaderPacket, *packetBuffer, error)
+	AppendPacket(buf *packetBuffer, maxPacketSize protocol.ByteCount, now time.Time, v protocol.Version) (shortHeaderPacket, error)
+	MaybePackProbePacket(protocol.EncryptionLevel, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error)
 	PackConnectionClose(*qerr.TransportError, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)
 	PackApplicationClose(*qerr.ApplicationError, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)
 	PackMTUProbePacket(ping ackhandler.Frame, size protocol.ByteCount, v protocol.Version) (shortHeaderPacket, *packetBuffer, error)
@@ -107,7 +108,7 @@ type sealingManager interface {
 type frameSource interface {
 	HasData() bool
 	AppendStreamFrames([]ackhandler.StreamFrame, protocol.ByteCount, protocol.Version) ([]ackhandler.StreamFrame, protocol.ByteCount)
-	AppendControlFrames([]ackhandler.Frame, protocol.ByteCount, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)
+	AppendControlFrames([]ackhandler.Frame, protocol.ByteCount, time.Time, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)
 }
 
 type ackFrameSource interface {
@@ -328,7 +329,7 @@ func (p *packetPacker) initialPaddingLen(frames []ackhandler.Frame, currentSize,
 // PackCoalescedPacket packs a new packet.
 // It packs an Initial / Handshake if there is data to send in these packet number spaces.
 // It should only be called before the handshake is confirmed.
-func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.ByteCount, v protocol.Version) (*coalescedPacket, error) {
+func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxSize protocol.ByteCount, now time.Time, v protocol.Version) (*coalescedPacket, error) {
 	var (
 		initialHdr, handshakeHdr, zeroRTTHdr                            *wire.ExtendedHeader
 		initialPayload, handshakePayload, zeroRTTPayload, oneRTTPayload payload
@@ -342,7 +343,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 	}
 	var size protocol.ByteCount
 	if initialSealer != nil {
-		initialHdr, initialPayload = p.maybeGetCryptoPacket(maxPacketSize-protocol.ByteCount(initialSealer.Overhead()), protocol.EncryptionInitial, onlyAck, true, v)
+		initialHdr, initialPayload = p.maybeGetCryptoPacket(maxSize-protocol.ByteCount(initialSealer.Overhead()), protocol.EncryptionInitial, onlyAck, true, v)
 		if initialPayload.length > 0 {
 			size += p.longHeaderPacketLength(initialHdr, initialPayload, v) + protocol.ByteCount(initialSealer.Overhead())
 		}
@@ -350,14 +351,14 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 
 	// Add a Handshake packet.
 	var handshakeSealer sealer
-	if (onlyAck && size == 0) || (!onlyAck && size < maxPacketSize-protocol.MinCoalescedPacketSize) {
+	if (onlyAck && size == 0) || (!onlyAck && size < maxSize-protocol.MinCoalescedPacketSize) {
 		var err error
 		handshakeSealer, err = p.cryptoSetup.GetHandshakeSealer()
 		if err != nil && err != handshake.ErrKeysDropped && err != handshake.ErrKeysNotYetAvailable {
 			return nil, err
 		}
 		if handshakeSealer != nil {
-			handshakeHdr, handshakePayload = p.maybeGetCryptoPacket(maxPacketSize-size-protocol.ByteCount(handshakeSealer.Overhead()), protocol.EncryptionHandshake, onlyAck, size == 0, v)
+			handshakeHdr, handshakePayload = p.maybeGetCryptoPacket(maxSize-size-protocol.ByteCount(handshakeSealer.Overhead()), protocol.EncryptionHandshake, onlyAck, size == 0, v)
 			if handshakePayload.length > 0 {
 				s := p.longHeaderPacketLength(handshakeHdr, handshakePayload, v) + protocol.ByteCount(handshakeSealer.Overhead())
 				size += s
@@ -370,7 +371,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 	var oneRTTSealer handshake.ShortHeaderSealer
 	var connID protocol.ConnectionID
 	var kp protocol.KeyPhaseBit
-	if (onlyAck && size == 0) || (!onlyAck && size < maxPacketSize-protocol.MinCoalescedPacketSize) {
+	if (onlyAck && size == 0) || (!onlyAck && size < maxSize-protocol.MinCoalescedPacketSize) {
 		var err error
 		oneRTTSealer, err = p.cryptoSetup.Get1RTTSealer()
 		if err != nil && err != handshake.ErrKeysDropped && err != handshake.ErrKeysNotYetAvailable {
@@ -381,7 +382,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 			connID = p.getDestConnID()
 			oneRTTPacketNumber, oneRTTPacketNumberLen = p.pnManager.PeekPacketNumber(protocol.Encryption1RTT)
 			hdrLen := wire.ShortHeaderLen(connID, oneRTTPacketNumberLen)
-			oneRTTPayload = p.maybeGetShortHeaderPacket(oneRTTSealer, hdrLen, maxPacketSize-size, onlyAck, size == 0, v)
+			oneRTTPayload = p.maybeGetShortHeaderPacket(oneRTTSealer, hdrLen, maxSize-size, onlyAck, size == 0, now, v)
 			if oneRTTPayload.length > 0 {
 				size += p.shortHeaderPacketLength(connID, oneRTTPacketNumberLen, oneRTTPayload) + protocol.ByteCount(oneRTTSealer.Overhead())
 			}
@@ -392,7 +393,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 				return nil, err
 			}
 			if zeroRTTSealer != nil {
-				zeroRTTHdr, zeroRTTPayload = p.maybeGetAppDataPacketFor0RTT(zeroRTTSealer, maxPacketSize-size, v)
+				zeroRTTHdr, zeroRTTPayload = p.maybeGetAppDataPacketFor0RTT(zeroRTTSealer, maxSize-size, now, v)
 				if zeroRTTPayload.length > 0 {
 					size += p.longHeaderPacketLength(zeroRTTHdr, zeroRTTPayload, v) + protocol.ByteCount(zeroRTTSealer.Overhead())
 				}
@@ -410,7 +411,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 		longHdrPackets: make([]*longHeaderPacket, 0, 3),
 	}
 	if initialPayload.length > 0 {
-		padding := p.initialPaddingLen(initialPayload.frames, size, maxPacketSize)
+		padding := p.initialPaddingLen(initialPayload.frames, size, maxSize)
 		cont, err := p.appendLongHeaderPacket(buffer, initialHdr, initialPayload, padding, protocol.EncryptionInitial, initialSealer, v)
 		if err != nil {
 			return nil, err
@@ -431,7 +432,7 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 		}
 		packet.longHdrPackets = append(packet.longHdrPackets, longHdrPacket)
 	} else if oneRTTPayload.length > 0 {
-		shp, err := p.appendShortHeaderPacket(buffer, connID, oneRTTPacketNumber, oneRTTPacketNumberLen, kp, oneRTTPayload, 0, maxPacketSize, oneRTTSealer, false, v)
+		shp, err := p.appendShortHeaderPacket(buffer, connID, oneRTTPacketNumber, oneRTTPacketNumberLen, kp, oneRTTPayload, 0, maxSize, oneRTTSealer, false, v)
 		if err != nil {
 			return nil, err
 		}
@@ -442,19 +443,25 @@ func (p *packetPacker) PackCoalescedPacket(onlyAck bool, maxPacketSize protocol.
 
 // PackAckOnlyPacket packs a packet containing only an ACK in the application data packet number space.
 // It should be called after the handshake is confirmed.
-func (p *packetPacker) PackAckOnlyPacket(maxPacketSize protocol.ByteCount, v protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
+func (p *packetPacker) PackAckOnlyPacket(maxSize protocol.ByteCount, now time.Time, v protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
 	buf := getPacketBuffer()
-	packet, err := p.appendPacket(buf, true, maxPacketSize, v)
+	packet, err := p.appendPacket(buf, true, maxSize, now, v)
 	return packet, buf, err
 }
 
 // AppendPacket packs a packet in the application data packet number space.
 // It should be called after the handshake is confirmed.
-func (p *packetPacker) AppendPacket(buf *packetBuffer, maxPacketSize protocol.ByteCount, v protocol.Version) (shortHeaderPacket, error) {
-	return p.appendPacket(buf, false, maxPacketSize, v)
+func (p *packetPacker) AppendPacket(buf *packetBuffer, maxSize protocol.ByteCount, now time.Time, v protocol.Version) (shortHeaderPacket, error) {
+	return p.appendPacket(buf, false, maxSize, now, v)
 }
 
-func (p *packetPacker) appendPacket(buf *packetBuffer, onlyAck bool, maxPacketSize protocol.ByteCount, v protocol.Version) (shortHeaderPacket, error) {
+func (p *packetPacker) appendPacket(
+	buf *packetBuffer,
+	onlyAck bool,
+	maxPacketSize protocol.ByteCount,
+	now time.Time,
+	v protocol.Version,
+) (shortHeaderPacket, error) {
 	sealer, err := p.cryptoSetup.Get1RTTSealer()
 	if err != nil {
 		return shortHeaderPacket{}, err
@@ -462,7 +469,7 @@ func (p *packetPacker) appendPacket(buf *packetBuffer, onlyAck bool, maxPacketSi
 	pn, pnLen := p.pnManager.PeekPacketNumber(protocol.Encryption1RTT)
 	connID := p.getDestConnID()
 	hdrLen := wire.ShortHeaderLen(connID, pnLen)
-	pl := p.maybeGetShortHeaderPacket(sealer, hdrLen, maxPacketSize, onlyAck, true, v)
+	pl := p.maybeGetShortHeaderPacket(sealer, hdrLen, maxPacketSize, onlyAck, true, now, v)
 	if pl.length == 0 {
 		return shortHeaderPacket{}, errNothingToPack
 	}
@@ -543,23 +550,39 @@ func (p *packetPacker) maybeGetCryptoPacket(maxPacketSize protocol.ByteCount, en
 	return hdr, pl
 }
 
-func (p *packetPacker) maybeGetAppDataPacketFor0RTT(sealer sealer, maxPacketSize protocol.ByteCount, v protocol.Version) (*wire.ExtendedHeader, payload) {
+func (p *packetPacker) maybeGetAppDataPacketFor0RTT(
+	sealer sealer,
+	maxPacketSize protocol.ByteCount,
+	now time.Time,
+	v protocol.Version,
+) (*wire.ExtendedHeader, payload) {
 	if p.perspective != protocol.PerspectiveClient {
 		return nil, payload{}
 	}
 
 	hdr := p.getLongHeader(protocol.Encryption0RTT, v)
 	maxPayloadSize := maxPacketSize - hdr.GetLength(v) - protocol.ByteCount(sealer.Overhead())
-	return hdr, p.maybeGetAppDataPacket(maxPayloadSize, false, false, v)
+	return hdr, p.maybeGetAppDataPacket(maxPayloadSize, false, false, now, v)
 }
 
-func (p *packetPacker) maybeGetShortHeaderPacket(sealer handshake.ShortHeaderSealer, hdrLen protocol.ByteCount, maxPacketSize protocol.ByteCount, onlyAck, ackAllowed bool, v protocol.Version) payload {
+func (p *packetPacker) maybeGetShortHeaderPacket(
+	sealer handshake.ShortHeaderSealer,
+	hdrLen, maxPacketSize protocol.ByteCount,
+	onlyAck, ackAllowed bool,
+	now time.Time,
+	v protocol.Version,
+) payload {
 	maxPayloadSize := maxPacketSize - hdrLen - protocol.ByteCount(sealer.Overhead())
-	return p.maybeGetAppDataPacket(maxPayloadSize, onlyAck, ackAllowed, v)
+	return p.maybeGetAppDataPacket(maxPayloadSize, onlyAck, ackAllowed, now, v)
 }
 
-func (p *packetPacker) maybeGetAppDataPacket(maxPayloadSize protocol.ByteCount, onlyAck, ackAllowed bool, v protocol.Version) payload {
-	pl := p.composeNextPacket(maxPayloadSize, onlyAck, ackAllowed, v)
+func (p *packetPacker) maybeGetAppDataPacket(
+	maxPayloadSize protocol.ByteCount,
+	onlyAck, ackAllowed bool,
+	now time.Time,
+	v protocol.Version,
+) payload {
+	pl := p.composeNextPacket(maxPayloadSize, onlyAck, ackAllowed, now, v)
 
 	// check if we have anything to send
 	if len(pl.frames) == 0 && len(pl.streamFrames) == 0 {
@@ -581,7 +604,12 @@ func (p *packetPacker) maybeGetAppDataPacket(maxPayloadSize protocol.ByteCount,
 	return pl
 }
 
-func (p *packetPacker) composeNextPacket(maxFrameSize protocol.ByteCount, onlyAck, ackAllowed bool, v protocol.Version) payload {
+func (p *packetPacker) composeNextPacket(
+	maxFrameSize protocol.ByteCount,
+	onlyAck, ackAllowed bool,
+	now time.Time,
+	v protocol.Version,
+) payload {
 	if onlyAck {
 		if ack := p.acks.GetAckFrame(protocol.Encryption1RTT, true); ack != nil {
 			return payload{ack: ack, length: ack.Length(v)}
@@ -641,7 +669,7 @@ func (p *packetPacker) composeNextPacket(maxFrameSize protocol.ByteCount, onlyAc
 	if hasData {
 		var lengthAdded protocol.ByteCount
 		startLen := len(pl.frames)
-		pl.frames, lengthAdded = p.framer.AppendControlFrames(pl.frames, maxFrameSize-pl.length, v)
+		pl.frames, lengthAdded = p.framer.AppendControlFrames(pl.frames, maxFrameSize-pl.length, now, v)
 		pl.length += lengthAdded
 		// add handlers for the control frames that were added
 		for i := startLen; i < len(pl.frames); i++ {
@@ -663,7 +691,12 @@ func (p *packetPacker) composeNextPacket(maxFrameSize protocol.ByteCount, onlyAc
 	return pl
 }
 
-func (p *packetPacker) MaybePackProbePacket(encLevel protocol.EncryptionLevel, maxPacketSize protocol.ByteCount, v protocol.Version) (*coalescedPacket, error) {
+func (p *packetPacker) MaybePackProbePacket(
+	encLevel protocol.EncryptionLevel,
+	maxPacketSize protocol.ByteCount,
+	now time.Time,
+	v protocol.Version,
+) (*coalescedPacket, error) {
 	if encLevel == protocol.Encryption1RTT {
 		s, err := p.cryptoSetup.Get1RTTSealer()
 		if err != nil {
@@ -673,7 +706,7 @@ func (p *packetPacker) MaybePackProbePacket(encLevel protocol.EncryptionLevel, m
 		connID := p.getDestConnID()
 		pn, pnLen := p.pnManager.PeekPacketNumber(protocol.Encryption1RTT)
 		hdrLen := wire.ShortHeaderLen(connID, pnLen)
-		pl := p.maybeGetAppDataPacket(maxPacketSize-protocol.ByteCount(s.Overhead())-hdrLen, false, true, v)
+		pl := p.maybeGetAppDataPacket(maxPacketSize-protocol.ByteCount(s.Overhead())-hdrLen, false, true, now, v)
 		if pl.length == 0 {
 			return nil, nil
 		}
diff --git a/receive_stream.go b/receive_stream.go
index b8535ef5..9b1b71cb 100644
--- a/receive_stream.go
+++ b/receive_stream.go
@@ -17,8 +17,8 @@ import (
 type receiveStreamI interface {
 	ReceiveStream
 
-	handleStreamFrame(*wire.StreamFrame) error
-	handleResetStreamFrame(*wire.ResetStreamFrame) error
+	handleStreamFrame(*wire.StreamFrame, time.Time) error
+	handleResetStreamFrame(*wire.ResetStreamFrame, time.Time) error
 	closeForShutdown(error)
 }
 
@@ -266,9 +266,9 @@ func (s *receiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) (queuedNe
 	return true
 }
 
-func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame) error {
+func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame, now time.Time) error {
 	s.mutex.Lock()
-	err := s.handleStreamFrameImpl(frame)
+	err := s.handleStreamFrameImpl(frame, now)
 	completed := s.isNewlyCompleted()
 	s.mutex.Unlock()
 
@@ -279,9 +279,9 @@ func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame) error {
 	return err
 }
 
-func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame) error {
+func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame, now time.Time) error {
 	maxOffset := frame.Offset + frame.DataLen()
-	if err := s.flowController.UpdateHighestReceived(maxOffset, frame.Fin); err != nil {
+	if err := s.flowController.UpdateHighestReceived(maxOffset, frame.Fin, now); err != nil {
 		return err
 	}
 	if frame.Fin {
@@ -297,9 +297,9 @@ func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame) error {
 	return nil
 }
 
-func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame) error {
+func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame, now time.Time) error {
 	s.mutex.Lock()
-	err := s.handleResetStreamFrameImpl(frame)
+	err := s.handleResetStreamFrameImpl(frame, now)
 	completed := s.isNewlyCompleted()
 	s.mutex.Unlock()
 
@@ -309,11 +309,11 @@ func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame) err
 	return err
 }
 
-func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame) error {
+func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame, now time.Time) error {
 	if s.closeForShutdownErr != nil {
 		return nil
 	}
-	if err := s.flowController.UpdateHighestReceived(frame.FinalSize, true); err != nil {
+	if err := s.flowController.UpdateHighestReceived(frame.FinalSize, true, now); err != nil {
 		return err
 	}
 	s.finalOffset = frame.FinalSize
@@ -333,7 +333,7 @@ func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame)
 	return nil
 }
 
-func (s *receiveStream) getControlFrame() (_ ackhandler.Frame, ok, hasMore bool) {
+func (s *receiveStream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok, hasMore bool) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
@@ -349,7 +349,10 @@ func (s *receiveStream) getControlFrame() (_ ackhandler.Frame, ok, hasMore bool)
 
 	s.queuedMaxStreamData = false
 	return ackhandler.Frame{
-		Frame: &wire.MaxStreamDataFrame{StreamID: s.streamID, MaximumStreamData: s.flowController.GetWindowUpdate()},
+		Frame: &wire.MaxStreamDataFrame{
+			StreamID:          s.streamID,
+			MaximumStreamData: s.flowController.GetWindowUpdate(now),
+		},
 	}, true, false
 }
 
diff --git a/send_stream.go b/send_stream.go
index 699c40ef..36f04260 100644
--- a/send_stream.go
+++ b/send_stream.go
@@ -473,7 +473,7 @@ func (s *sendStream) handleStopSendingFrame(frame *wire.StopSendingFrame) {
 	s.cancelWriteImpl(frame.ErrorCode, true)
 }
 
-func (s *sendStream) getControlFrame() (_ ackhandler.Frame, ok, hasMore bool) {
+func (s *sendStream) getControlFrame(time.Time) (_ ackhandler.Frame, ok, hasMore bool) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
diff --git a/stream.go b/stream.go
index 1ed26323..ef21cd41 100644
--- a/stream.go
+++ b/stream.go
@@ -52,8 +52,8 @@ type streamI interface {
 	Stream
 	closeForShutdown(error)
 	// for receiving
-	handleStreamFrame(*wire.StreamFrame) error
-	handleResetStreamFrame(*wire.ResetStreamFrame) error
+	handleStreamFrame(*wire.StreamFrame, time.Time) error
+	handleResetStreamFrame(*wire.ResetStreamFrame, time.Time) error
 	// for sending
 	hasData() bool
 	handleStopSendingFrame(*wire.StopSendingFrame)
@@ -131,12 +131,12 @@ func (s *stream) Close() error {
 	return s.sendStream.Close()
 }
 
-func (s *stream) getControlFrame() (_ ackhandler.Frame, ok, hasMore bool) {
-	f, ok, _ := s.sendStream.getControlFrame()
+func (s *stream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok, hasMore bool) {
+	f, ok, _ := s.sendStream.getControlFrame(now)
 	if ok {
 		return f, true, true
 	}
-	return s.receiveStream.getControlFrame()
+	return s.receiveStream.getControlFrame(now)
 }
 
 func (s *stream) SetDeadline(t time.Time) error {
diff --git a/sys_conn.go b/sys_conn.go
index 71cc4607..811131d9 100644
--- a/sys_conn.go
+++ b/sys_conn.go
@@ -58,8 +58,8 @@ func wrapConn(pc net.PacketConn) (rawConn, error) {
 			return nil, err
 		}
 
+		// only set DF on UDP sockets
 		if _, ok := pc.LocalAddr().(*net.UDPAddr); ok {
-			// Only set DF on sockets that we expect to be able to handle that configuration.
 			var err error
 			supportsDF, err = setDF(rawConn)
 			if err != nil {
diff --git a/sys_conn_df_darwin.go b/sys_conn_df_darwin.go
index b51cd8f1..8ed273ee 100644
--- a/sys_conn_df_darwin.go
+++ b/sys_conn_df_darwin.go
@@ -4,47 +4,67 @@ package quic
 
 import (
 	"errors"
+	"fmt"
 	"strconv"
 	"strings"
 	"syscall"
 
 	"golang.org/x/sys/unix"
+)
 
-	"github.com/quic-go/quic-go/internal/utils"
+// for macOS versions, see https://en.wikipedia.org/wiki/Darwin_(operating_system)#Darwin_20_onwards
+const (
+	macOSVersion11 = 20
+	macOSVersion15 = 24
 )
 
 func setDF(rawConn syscall.RawConn) (bool, error) {
-	// Setting DF bit is only supported from macOS11
+	// Setting DF bit is only supported from macOS 11.
 	// https://github.com/chromium/chromium/blob/117.0.5881.2/net/socket/udp_socket_posix.cc#L555
-	if supportsDF, err := isAtLeastMacOS11(); !supportsDF || err != nil {
+	version, err := getMacOSVersion()
+	if err != nil || version < macOSVersion11 {
 		return false, err
 	}
 
-	// Enabling IP_DONTFRAG will force the kernel to return "sendto: message too long"
-	// and the datagram will not be fragmented
-	var errDFIPv4, errDFIPv6 error
+	var controlErr error
+	var disableDF bool
 	if err := rawConn.Control(func(fd uintptr) {
-		errDFIPv4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_DONTFRAG, 1)
-		errDFIPv6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
+		addr, err := unix.Getsockname(int(fd))
+		if err != nil {
+			controlErr = fmt.Errorf("getsockname: %w", err)
+			return
+		}
+
+		// Dual-stack sockets are effectively IPv6 sockets (with IPV6_ONLY set to 0).
+		// On macOS, the DF bit on dual-stack sockets is controlled by the IPV6_DONTFRAG option.
+		// See https://datatracker.ietf.org/doc/draft-seemann-tsvwg-udp-fragmentation/ for details.
+		switch addr.(type) {
+		case *unix.SockaddrInet4:
+			controlErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_DONTFRAG, 1)
+		case *unix.SockaddrInet6:
+			controlErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
+
+			// Setting the DF bit on dual-stack sockets works since macOS Sequoia.
+			// Disable DF on dual-stack sockets before Sequoia.
+			if version < macOSVersion15 {
+				// check if this is a dual-stack socket by reading the IPV6_V6ONLY flag
+				v6only, err := unix.GetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_V6ONLY)
+				if err != nil {
+					controlErr = fmt.Errorf("getting IPV6_V6ONLY: %w", err)
+					return
+				}
+				disableDF = v6only == 0
+			}
+		default:
+			controlErr = fmt.Errorf("unknown address type: %T", addr)
+		}
 	}); err != nil {
 		return false, err
 	}
-	switch {
-	case errDFIPv4 == nil && errDFIPv6 == nil:
-		utils.DefaultLogger.Debugf("Setting DF for IPv4 and IPv6.")
-	case errDFIPv4 == nil && errDFIPv6 != nil:
-		utils.DefaultLogger.Debugf("Setting DF for IPv4.")
-	case errDFIPv4 != nil && errDFIPv6 == nil:
-		utils.DefaultLogger.Debugf("Setting DF for IPv6.")
-		// On macOS, the syscall for setting DF bit for IPv4 fails on dual-stack listeners.
-		// Treat the connection as not having DF enabled, even though the DF bit will be set
-		// when used for IPv6.
-		// See https://github.com/quic-go/quic-go/issues/3793 for details.
-		return false, nil
-	case errDFIPv4 != nil && errDFIPv6 != nil:
-		return false, errors.New("setting DF failed for both IPv4 and IPv6")
+	if controlErr != nil {
+		return false, controlErr
 	}
-	return true, nil
+	return !disableDF, nil
 }
 
 func isSendMsgSizeErr(err error) bool {
@@ -53,22 +73,20 @@ func isSendMsgSizeErr(err error) bool {
 
 func isRecvMsgSizeErr(error) bool { return false }
 
-func isAtLeastMacOS11() (bool, error) {
+func getMacOSVersion() (int, error) {
 	uname := &unix.Utsname{}
-	err := unix.Uname(uname)
-	if err != nil {
-		return false, err
+	if err := unix.Uname(uname); err != nil {
+		return 0, err
 	}
 
 	release := string(uname.Release[:])
-	if idx := strings.Index(release, "."); idx != -1 {
-		version, err := strconv.Atoi(release[:idx])
-		if err != nil {
-			return false, err
-		}
-		// Darwin version 20 is macOS version 11
-		// https://en.wikipedia.org/wiki/Darwin_(operating_system)#Darwin_20_onwards
-		return version >= 20, nil
+	idx := strings.Index(release, ".")
+	if idx == -1 {
+		return 0, nil
+	}
+	version, err := strconv.Atoi(release[:idx])
+	if err != nil {
+		return 0, err
 	}
-	return false, nil
+	return version, nil
 }
diff --git a/sys_conn_df_linux.go b/sys_conn_df_linux.go
index f09eaa5d..b09a2394 100644
--- a/sys_conn_df_linux.go
+++ b/sys_conn_df_linux.go
@@ -16,8 +16,8 @@ func setDF(rawConn syscall.RawConn) (bool, error) {
 	// and the datagram will not be fragmented
 	var errDFIPv4, errDFIPv6 error
 	if err := rawConn.Control(func(fd uintptr) {
-		errDFIPv4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_MTU_DISCOVER, unix.IP_PMTUDISC_DO)
-		errDFIPv6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_MTU_DISCOVER, unix.IPV6_PMTUDISC_DO)
+		errDFIPv4 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_MTU_DISCOVER, unix.IP_PMTUDISC_PROBE)
+		errDFIPv6 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_MTU_DISCOVER, unix.IPV6_PMTUDISC_PROBE)
 	}); err != nil {
 		return false, err
 	}
diff --git a/sys_conn_df_windows.go b/sys_conn_df_windows.go
index e27635ec..4c140f00 100644
--- a/sys_conn_df_windows.go
+++ b/sys_conn_df_windows.go
@@ -12,21 +12,19 @@ import (
 )
 
 const (
-	// IP_DONTFRAGMENT controls the Don't Fragment (DF) bit.
-	//
-	// It's the same code point for both IPv4 and IPv6 on Windows.
-	// https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Networking/WinSock/constant.IP_DONTFRAG.html
-	// https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Networking/WinSock/constant.IPV6_DONTFRAG.html
-	//
+	// https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Networking/WinSock/constant.IP_DONTFRAGMENT.html
 	//nolint:stylecheck
 	IP_DONTFRAGMENT = 14
+	// https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Networking/WinSock/constant.IPV6_DONTFRAG.html
+	//nolint:stylecheck
+	IPV6_DONTFRAG = 14
 )
 
 func setDF(rawConn syscall.RawConn) (bool, error) {
 	var errDFIPv4, errDFIPv6 error
 	if err := rawConn.Control(func(fd uintptr) {
 		errDFIPv4 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_DONTFRAGMENT, 1)
-		errDFIPv6 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IP_DONTFRAGMENT, 1)
+		errDFIPv6 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_DONTFRAG, 1)
 	}); err != nil {
 		return false, err
 	}


Test patch:
diff --git a/connection_test.go b/connection_test.go
index 6ece26dc..bbe0fe6b 100644
--- a/connection_test.go
+++ b/connection_test.go
@@ -92,7 +92,7 @@ var _ = Describe("Connection", func() {
 	}
 
 	expectAppendPacket := func(packer *MockPacker, p shortHeaderPacket, b []byte) *MockPackerAppendPacketCall {
-		return packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), Version1).DoAndReturn(func(buf *packetBuffer, _ protocol.ByteCount, _ protocol.Version) (shortHeaderPacket, error) {
+		return packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), Version1).DoAndReturn(func(buf *packetBuffer, _ protocol.ByteCount, _ time.Time, _ protocol.Version) (shortHeaderPacket, error) {
 			buf.Data = append(buf.Data, b...)
 			return p, nil
 		})
@@ -159,9 +159,10 @@ var _ = Describe("Connection", func() {
 					Data:     []byte{0xde, 0xca, 0xfb, 0xad},
 				}
 				str := NewMockReceiveStreamI(mockCtrl)
-				str.EXPECT().handleStreamFrame(f)
+				now := time.Now()
+				str.EXPECT().handleStreamFrame(f, now)
 				streamManager.EXPECT().GetOrOpenReceiveStream(protocol.StreamID(5)).Return(str, nil)
-				Expect(conn.handleStreamFrame(f)).To(Succeed())
+				Expect(conn.handleStreamFrame(f, now)).To(Succeed())
 			})
 
 			It("returns errors", func() {
@@ -171,9 +172,10 @@ var _ = Describe("Connection", func() {
 					Data:     []byte{0xde, 0xca, 0xfb, 0xad},
 				}
 				str := NewMockReceiveStreamI(mockCtrl)
-				str.EXPECT().handleStreamFrame(f).Return(testErr)
+				now := time.Now()
+				str.EXPECT().handleStreamFrame(f, now).Return(testErr)
 				streamManager.EXPECT().GetOrOpenReceiveStream(protocol.StreamID(5)).Return(str, nil)
-				Expect(conn.handleStreamFrame(f)).To(MatchError(testErr))
+				Expect(conn.handleStreamFrame(f, now)).To(MatchError(testErr))
 			})
 
 			It("ignores STREAM frames for closed streams", func() {
@@ -181,7 +183,7 @@ var _ = Describe("Connection", func() {
 				Expect(conn.handleStreamFrame(&wire.StreamFrame{
 					StreamID: 5,
 					Data:     []byte("foobar"),
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 			})
 		})
 
@@ -205,9 +207,9 @@ var _ = Describe("Connection", func() {
 				}
 				str := NewMockReceiveStreamI(mockCtrl)
 				streamManager.EXPECT().GetOrOpenReceiveStream(protocol.StreamID(555)).Return(str, nil)
-				str.EXPECT().handleResetStreamFrame(f)
-				err := conn.handleResetStreamFrame(f)
-				Expect(err).ToNot(HaveOccurred())
+				now := time.Now()
+				str.EXPECT().handleResetStreamFrame(f, now)
+				Expect(conn.handleResetStreamFrame(f, now)).To(Succeed())
 			})
 
 			It("returns errors", func() {
@@ -218,9 +220,9 @@ var _ = Describe("Connection", func() {
 				testErr := errors.New("flow control violation")
 				str := NewMockReceiveStreamI(mockCtrl)
 				streamManager.EXPECT().GetOrOpenReceiveStream(protocol.StreamID(7)).Return(str, nil)
-				str.EXPECT().handleResetStreamFrame(f).Return(testErr)
-				err := conn.handleResetStreamFrame(f)
-				Expect(err).To(MatchError(testErr))
+				now := time.Now()
+				str.EXPECT().handleResetStreamFrame(f, now).Return(testErr)
+				Expect(conn.handleResetStreamFrame(f, now)).To(MatchError(testErr))
 			})
 
 			It("ignores RESET_STREAM frames for closed streams", func() {
@@ -228,7 +230,7 @@ var _ = Describe("Connection", func() {
 				Expect(conn.handleFrame(&wire.ResetStreamFrame{
 					StreamID:  3,
 					ErrorCode: 42,
-				}, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+				}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			})
 		})
 
@@ -262,7 +264,7 @@ var _ = Describe("Connection", func() {
 				Expect(conn.handleFrame(&wire.MaxStreamDataFrame{
 					StreamID:          10,
 					MaximumStreamData: 1337,
-				}, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+				}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			})
 		})
 
@@ -295,7 +297,7 @@ var _ = Describe("Connection", func() {
 				Expect(conn.handleFrame(&wire.StopSendingFrame{
 					StreamID:  3,
 					ErrorCode: 1337,
-				}, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+				}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			})
 		})
 
@@ -304,25 +306,25 @@ var _ = Describe("Connection", func() {
 			Expect(conn.handleFrame(&wire.NewConnectionIDFrame{
 				SequenceNumber: 10,
 				ConnectionID:   connID,
-			}, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+			}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			Expect(conn.connIDManager.queue.Back().Value.ConnectionID).To(Equal(connID))
 		})
 
 		It("handles PING frames", func() {
-			err := conn.handleFrame(&wire.PingFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.PingFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).NotTo(HaveOccurred())
 		})
 
 		It("rejects PATH_RESPONSE frames", func() {
-			err := conn.handleFrame(&wire.PathResponseFrame{Data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.PathResponseFrame{Data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).To(MatchError("unexpected PATH_RESPONSE frame"))
 		})
 
 		It("handles PATH_CHALLENGE frames", func() {
 			data := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
-			err := conn.handleFrame(&wire.PathChallengeFrame{Data: data}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.PathChallengeFrame{Data: data}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).ToNot(HaveOccurred())
-			frames, _ := conn.framer.AppendControlFrames(nil, 1000, protocol.Version1)
+			frames, _ := conn.framer.AppendControlFrames(nil, 1000, time.Now(), protocol.Version1)
 			Expect(frames).To(Equal([]ackhandler.Frame{{Frame: &wire.PathResponseFrame{Data: data}}}))
 		})
 
@@ -334,17 +336,17 @@ var _ = Describe("Connection", func() {
 		})
 
 		It("handles BLOCKED frames", func() {
-			err := conn.handleFrame(&wire.DataBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.DataBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).NotTo(HaveOccurred())
 		})
 
 		It("handles STREAM_BLOCKED frames", func() {
-			err := conn.handleFrame(&wire.StreamDataBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.StreamDataBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).NotTo(HaveOccurred())
 		})
 
 		It("handles STREAMS_BLOCKED frames", func() {
-			err := conn.handleFrame(&wire.StreamsBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{})
+			err := conn.handleFrame(&wire.StreamsBlockedFrame{}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())
 			Expect(err).NotTo(HaveOccurred())
 		})
 
@@ -373,7 +375,7 @@ var _ = Describe("Connection", func() {
 			Expect(conn.handleFrame(&wire.ConnectionCloseFrame{
 				ErrorCode:    uint64(qerr.StreamLimitError),
 				ReasonPhrase: "foobar",
-			}, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+			}, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			Eventually(conn.Context().Done()).Should(BeClosed())
 		})
 
@@ -404,7 +406,7 @@ var _ = Describe("Connection", func() {
 				ReasonPhrase:       "foobar",
 				IsApplicationError: true,
 			}
-			Expect(conn.handleFrame(ccf, protocol.Encryption1RTT, protocol.ConnectionID{})).To(Succeed())
+			Expect(conn.handleFrame(ccf, protocol.Encryption1RTT, protocol.ConnectionID{}, time.Now())).To(Succeed())
 			Eventually(conn.Context().Done()).Should(BeClosed())
 			Expect(context.Cause(conn.Context())).To(MatchError(testErr))
 		})
@@ -612,7 +614,7 @@ var _ = Describe("Connection", func() {
 			cryptoSetup.EXPECT().Close()
 			conn.sentPacketHandler = sph
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 1}, []byte("foobar"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
 			runConn()
 			conn.queueControlFrame(&wire.PingFrame{})
 			conn.scheduleSending()
@@ -843,7 +845,7 @@ var _ = Describe("Connection", func() {
 				return pn, protocol.PacketNumberLen2, protocol.KeyPhaseZero, []byte{0} /* PADDING frame */, nil
 			}).Times(3)
 			tracer.EXPECT().ReceivedShortHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(3)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version) // only expect a single call
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version) // only expect a single call
 
 			for i := 0; i < 3; i++ {
 				conn.handlePacket(getShortHeaderPacket(srcConnID, 0x1337+protocol.PacketNumber(i), []byte("foobar")))
@@ -878,7 +880,7 @@ var _ = Describe("Connection", func() {
 				return pn, protocol.PacketNumberLen4, protocol.KeyPhaseZero, []byte{0} /* PADDING frame */, nil
 			}).Times(3)
 			tracer.EXPECT().ReceivedShortHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(3)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).Times(3)
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).Times(3)
 
 			for i := 0; i < 3; i++ {
 				conn.handlePacket(getShortHeaderPacket(srcConnID, 0x1337+protocol.PacketNumber(i), []byte("foobar")))
@@ -1235,7 +1237,7 @@ var _ = Describe("Connection", func() {
 				KeyPhase:        protocol.KeyPhaseOne,
 			}
 			expectAppendPacket(packer, p, []byte("foobar"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
 			sent := make(chan struct{})
 			sender.EXPECT().WouldBlock().AnyTimes()
 			sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(*packetBuffer, uint16, protocol.ECN) { close(sent) })
@@ -1255,7 +1257,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
 			sph.EXPECT().ECNMode(true).AnyTimes()
 			runConn()
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
 			conn.scheduleSending()
 			time.Sleep(50 * time.Millisecond) // make sure there are no calls to mconn.Write()
 		})
@@ -1266,7 +1268,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAck)
 			sph.EXPECT().ECNMode(gomock.Any()).Return(protocol.ECT1).AnyTimes()
 			done := make(chan struct{})
-			packer.EXPECT().PackCoalescedPacket(true, gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error) {
+			packer.EXPECT().PackCoalescedPacket(true, gomock.Any(), gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error) {
 				close(done)
 				return nil, nil
 			})
@@ -1284,9 +1286,12 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
 			fc := mocks.NewMockConnectionFlowController(mockCtrl)
 			fc.EXPECT().IsNewlyBlocked().Return(true, protocol.ByteCount(1337))
-			fc.EXPECT().GetWindowUpdate()
+			fc.EXPECT().GetWindowUpdate(gomock.Any()).Do(func(t time.Time) protocol.ByteCount {
+				Expect(t).To(BeTemporally("~", time.Now(), time.Second))
+				return 0
+			})
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 13}, []byte("foobar"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
 			conn.connFlowController = fc
 			runConn()
 			sent := make(chan struct{})
@@ -1294,7 +1299,7 @@ var _ = Describe("Connection", func() {
 			tracer.EXPECT().SentShortHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), nil, []logging.Frame{})
 			conn.scheduleSending()
 			Eventually(sent).Should(BeClosed())
-			frames, _ := conn.framer.AppendControlFrames(nil, 1000, protocol.Version1)
+			frames, _ := conn.framer.AppendControlFrames(nil, 1000, time.Now(), protocol.Version1)
 			Expect(frames).To(Equal([]ackhandler.Frame{{Frame: &logging.DataBlockedFrame{MaximumData: 1337}}}))
 		})
 
@@ -1336,7 +1341,7 @@ var _ = Describe("Connection", func() {
 					sph.EXPECT().QueueProbePacket(encLevel)
 					sph.EXPECT().ECNMode(gomock.Any())
 					p := getCoalescedPacket(123, encLevel)
-					packer.EXPECT().MaybePackProbePacket(encLevel, gomock.Any(), conn.version).Return(p, nil)
+					packer.EXPECT().MaybePackProbePacket(encLevel, gomock.Any(), gomock.Any(), conn.version).Return(p, nil)
 					sph.EXPECT().SentPacket(gomock.Any(), protocol.PacketNumber(123), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
 					conn.sentPacketHandler = sph
 					runConn()
@@ -1359,7 +1364,7 @@ var _ = Describe("Connection", func() {
 					sph.EXPECT().ECNMode(gomock.Any()).Return(protocol.ECT0)
 					sph.EXPECT().QueueProbePacket(encLevel).Return(false)
 					p := getCoalescedPacket(123, encLevel)
-					packer.EXPECT().MaybePackProbePacket(encLevel, gomock.Any(), conn.version).Return(p, nil)
+					packer.EXPECT().MaybePackProbePacket(encLevel, gomock.Any(), gomock.Any(), conn.version).Return(p, nil)
 					sph.EXPECT().SentPacket(gomock.Any(), protocol.PacketNumber(123), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
 					runConn()
 					sent := make(chan struct{})
@@ -1448,7 +1453,7 @@ var _ = Describe("Connection", func() {
 			rand.Read(payload2)
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 10}, payload1)
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 11}, payload2)
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack)
 			sender.EXPECT().WouldBlock().AnyTimes()
 			sender.EXPECT().Send(gomock.Any(), uint16(conn.maxPacketSize()), gomock.Any()).Do(func(b *packetBuffer, _ uint16, _ protocol.ECN) {
 				Expect(b.Data).To(Equal(append(payload1, payload2...)))
@@ -1533,7 +1538,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).Times(2)
 			sph.EXPECT().ECNMode(gomock.Any()).Times(2)
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 10}, []byte("packet10"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 			sender.EXPECT().WouldBlock().AnyTimes()
 			sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any())
 			go func() {
@@ -1551,7 +1556,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().TimeUntilSend().Return(time.Now().Add(time.Hour))
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendPacingLimited)
 			sph.EXPECT().ECNMode(gomock.Any())
-			packer.EXPECT().PackAckOnlyPacket(gomock.Any(), conn.version).Return(shortHeaderPacket{PacketNumber: 123}, getPacketBuffer(), nil)
+			packer.EXPECT().PackAckOnlyPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{PacketNumber: 123}, getPacketBuffer(), nil)
 
 			sender.EXPECT().WouldBlock().AnyTimes()
 			sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any())
@@ -1639,7 +1644,6 @@ var _ = Describe("Connection", func() {
 		})
 
 		for _, withGSO := range []bool{false, true} {
-			withGSO := withGSO
 			It(fmt.Sprintf("doesn't try to send if the send queue is full: %t", withGSO), func() {
 				if withGSO {
 					enableGSO()
@@ -1662,7 +1666,7 @@ var _ = Describe("Connection", func() {
 				sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
 				sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
 				expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 1000}, []byte("packet1000"))
-				packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+				packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 				sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(*packetBuffer, uint16, protocol.ECN) { close(written) })
 				available <- struct{}{}
 				Eventually(written).Should(BeClosed())
@@ -1687,7 +1691,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
 			sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 10}, []byte("packet10"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 			sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(*packetBuffer, uint16, protocol.ECN) { close(written) })
 
 			conn.scheduleSending()
@@ -1723,7 +1727,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
 			sender.EXPECT().WouldBlock().AnyTimes()
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 1001}, []byte("packet1001"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 			sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(*packetBuffer, uint16, protocol.ECN) { written <- struct{}{} })
 			available <- struct{}{}
 			Eventually(written).Should(Receive())
@@ -1737,7 +1741,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
 			sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
 			sender.EXPECT().WouldBlock().AnyTimes()
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 			// don't EXPECT any calls to mconn.Write()
 			go func() {
 				defer GinkgoRecover()
@@ -1811,7 +1815,7 @@ var _ = Describe("Connection", func() {
 			sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
 			conn.sentPacketHandler = sph
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 1}, []byte("packet1"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 
 			go func() {
 				defer GinkgoRecover()
@@ -1831,7 +1835,7 @@ var _ = Describe("Connection", func() {
 
 		It("sets the timer to the ack timer", func() {
 			expectAppendPacket(packer, shortHeaderPacket{PacketNumber: 1234}, []byte("packet1234"))
-			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
+			packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack)
 			sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
 			sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
 			sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
@@ -1864,7 +1868,7 @@ var _ = Describe("Connection", func() {
 		conn.sentPacketHandler = sph
 		buffer := getPacketBuffer()
 		buffer.Data = append(buffer.Data, []byte("foobar")...)
-		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).Return(&coalescedPacket{
+		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).Return(&coalescedPacket{
 			buffer: buffer,
 			longHdrPackets: []*longHeaderPacket{
 				{
@@ -1883,7 +1887,7 @@ var _ = Describe("Connection", func() {
 				},
 			},
 		}, nil)
-		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 
 		sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
 		sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
@@ -1928,7 +1932,7 @@ var _ = Describe("Connection", func() {
 	})
 
 	It("cancels the HandshakeComplete context when the handshake completes", func() {
-		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 		sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
 		conn.sentPacketHandler = sph
 		tracer.EXPECT().DroppedEncryptionLevel(protocol.EncryptionHandshake)
@@ -1950,7 +1954,7 @@ var _ = Describe("Connection", func() {
 
 	It("sends a session ticket when the handshake completes", func() {
 		const size = protocol.MaxPostHandshakeCryptoFrameSize * 3 / 2
-		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 		connRunner.EXPECT().Retire(clientDestConnID)
 		conn.sentPacketHandler.DropPackets(protocol.EncryptionInitial)
 		tracer.EXPECT().DroppedEncryptionLevel(protocol.EncryptionHandshake)
@@ -1964,7 +1968,7 @@ var _ = Describe("Connection", func() {
 		Expect(conn.handleHandshakeComplete()).To(Succeed())
 		var frames []ackhandler.Frame
 		Eventually(func() []ackhandler.Frame {
-			frames, _ = conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			frames, _ = conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			return frames
 		}).ShouldNot(BeEmpty())
 		var count int
@@ -1980,7 +1984,7 @@ var _ = Describe("Connection", func() {
 	})
 
 	It("doesn't cancel the HandshakeComplete context when the handshake fails", func() {
-		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+		packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 		streamManager.EXPECT().CloseWithError(gomock.Any())
 		expectReplaceWithClosed()
 		packer.EXPECT().PackConnectionClose(gomock.Any(), gomock.Any(), conn.version).Return(&coalescedPacket{buffer: getPacketBuffer()}, nil)
@@ -2015,14 +2019,14 @@ var _ = Describe("Connection", func() {
 		conn.sentPacketHandler = sph
 		done := make(chan struct{})
 		connRunner.EXPECT().Retire(clientDestConnID)
-		packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).DoAndReturn(func(_ *packetBuffer, _ protocol.ByteCount, v protocol.Version) (shortHeaderPacket, error) {
-			frames, _ := conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, v)
+		packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).DoAndReturn(func(_ *packetBuffer, _ protocol.ByteCount, _ time.Time, v protocol.Version) (shortHeaderPacket, error) {
+			frames, _ := conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), v)
 			Expect(frames).ToNot(BeEmpty())
 			Expect(frames[0].Frame).To(BeEquivalentTo(&wire.HandshakeDoneFrame{}))
 			defer close(done)
 			return shortHeaderPacket{}, nil
 		})
-		packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
+		packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), conn.version).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
 		tracer.EXPECT().DroppedEncryptionLevel(protocol.EncryptionHandshake)
 		sph.EXPECT().DropPackets(protocol.EncryptionHandshake)
 		go func() {
@@ -2107,7 +2111,8 @@ var _ = Describe("Connection", func() {
 				InitialSourceConnectionID: destConnID,
 			}
 			streamManager.EXPECT().UpdateLimits(params)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).MaxTimes(3)
+
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).MaxTimes(3)
 			Expect(conn.earlyConnReady()).ToNot(BeClosed())
 			tracer.EXPECT().ReceivedTransportParameters(params)
 			conn.handleTransportParameters(params)
@@ -2157,7 +2162,7 @@ var _ = Describe("Connection", func() {
 			setRemoteIdleTimeout(5 * time.Second)
 			conn.lastPacketReceivedTime = time.Now().Add(-5 * time.Second / 2)
 			sent := make(chan struct{})
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error) {
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error) {
 				close(sent)
 				return nil, nil
 			})
@@ -2170,7 +2175,7 @@ var _ = Describe("Connection", func() {
 			setRemoteIdleTimeout(time.Hour)
 			conn.lastPacketReceivedTime = time.Now().Add(-protocol.MaxKeepAliveInterval).Add(-time.Millisecond)
 			sent := make(chan struct{})
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error) {
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error) {
 				close(sent)
 				return nil, nil
 			})
@@ -2203,7 +2208,7 @@ var _ = Describe("Connection", func() {
 			pto := conn.rttStats.PTO(true)
 			conn.lastPacketReceivedTime = time.Now()
 			sentPingTimeChan := make(chan time.Time)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error) {
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).Do(func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error) {
 				sentPingTimeChan <- time.Now()
 				return nil, nil
 			})
@@ -2306,7 +2311,7 @@ var _ = Describe("Connection", func() {
 
 		It("closes the connection due to the idle timeout before handshake", func() {
 			conn.config.HandshakeIdleTimeout = scaleDuration(25 * time.Millisecond)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 			connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
 			cryptoSetup.EXPECT().Close()
 			gomock.InOrder(
@@ -2334,7 +2339,7 @@ var _ = Describe("Connection", func() {
 
 		It("closes the connection due to the idle timeout after handshake", func() {
 			conn.sentPacketHandler.DropPackets(protocol.EncryptionInitial)
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 			gomock.InOrder(
 				connRunner.EXPECT().Retire(clientDestConnID),
 				connRunner.EXPECT().Remove(gomock.Any()),
@@ -2392,7 +2397,7 @@ var _ = Describe("Connection", func() {
 		})
 
 		It("times out earliest after 3 times the PTO", func() {
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).AnyTimes()
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).AnyTimes()
 			connRunner.EXPECT().Retire(gomock.Any()).AnyTimes()
 			connRunner.EXPECT().Remove(gomock.Any()).Times(2)
 			cryptoSetup.EXPECT().Close()
@@ -2609,7 +2614,7 @@ var _ = Describe("Client Connection", func() {
 		})
 		conn.unpacker = unpacker
 		done := make(chan struct{})
-		packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(onlyAck bool, maxPacketSize protocol.ByteCount, v protocol.Version) (*coalescedPacket, error) {
+		packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error) {
 			close(done)
 			return nil, nil
 		})
@@ -2785,7 +2790,7 @@ var _ = Describe("Client Connection", func() {
 
 		It("closes when no matching version is found", func() {
 			errChan := make(chan error, 1)
-			packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any()).MaxTimes(1)
+			packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).MaxTimes(1)
 			go func() {
 				defer GinkgoRecover()
 				cryptoSetup.EXPECT().StartHandshake(gomock.Any()).MaxTimes(1)
@@ -2900,7 +2905,7 @@ var _ = Describe("Client Connection", func() {
 			errChan = make(chan error, 1)
 			paramsChan = make(chan *wire.TransportParameters, 1)
 			closed = false
-			packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
+			packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
 			go func() {
 				defer GinkgoRecover()
 				cryptoSetup.EXPECT().StartHandshake(gomock.Any()).MaxTimes(1)
@@ -2961,13 +2966,13 @@ var _ = Describe("Client Connection", func() {
 					StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
 				},
 			}
-			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), conn.version).MaxTimes(1)
+			packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), conn.version).MaxTimes(1)
 			processed := make(chan struct{})
 			tracer.EXPECT().ReceivedTransportParameters(params).Do(func(*wire.TransportParameters) { close(processed) })
 			paramsChan <- params
 			Eventually(processed).Should(BeClosed())
 			// make sure the connection ID is not retired
-			cf, _ := conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			cf, _ := conn.framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			Expect(cf).To(BeEmpty())
 			connRunner.EXPECT().AddResetToken(protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, conn)
 			Expect(conn.connIDManager.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
diff --git a/framer_test.go b/framer_test.go
index a8cf8328..8d767e5c 100644
--- a/framer_test.go
+++ b/framer_test.go
@@ -2,6 +2,7 @@ package quic
 
 import (
 	"bytes"
+	"time"
 
 	"golang.org/x/exp/rand"
 
@@ -40,7 +41,7 @@ var _ = Describe("Framer", func() {
 			msf := &wire.MaxStreamsFrame{MaxStreamNum: 0x1337}
 			framer.QueueControlFrame(pc)
 			framer.QueueControlFrame(msf)
-			frames, length := framer.AppendControlFrames(nil, 1000, protocol.Version1)
+			frames, length := framer.AppendControlFrames(nil, 1000, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(2))
 			fs := []wire.Frame{frames[0].Frame, frames[1].Frame}
 			Expect(fs).To(ContainElement(pc))
@@ -53,7 +54,7 @@ var _ = Describe("Framer", func() {
 			f := &wire.MaxDataFrame{MaximumData: 0x42}
 			framer.QueueControlFrame(f)
 			Expect(framer.HasData()).To(BeTrue())
-			frames, _ := framer.AppendControlFrames(nil, 1000, protocol.Version1)
+			frames, _ := framer.AppendControlFrames(nil, 1000, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(1))
 			Expect(framer.HasData()).To(BeFalse())
 		})
@@ -62,7 +63,7 @@ var _ = Describe("Framer", func() {
 			ping := &wire.PingFrame{}
 			pc := &wire.PathChallengeFrame{Data: [8]byte{1, 2, 3, 4, 6, 7, 8}}
 			framer.QueueControlFrame(pc)
-			frames, length := framer.AppendControlFrames([]ackhandler.Frame{{Frame: ping}}, 1000, protocol.Version1)
+			frames, length := framer.AppendControlFrames([]ackhandler.Frame{{Frame: ping}}, 1000, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(2))
 			Expect(frames[0].Frame).To(Equal(ping))
 			Expect(frames[1].Frame).To(Equal(pc))
@@ -76,9 +77,10 @@ var _ = Describe("Framer", func() {
 			framer.AddStreamWithControlFrames(10, str)
 			mdf1 := &wire.MaxStreamDataFrame{MaximumStreamData: 1337}
 			mdf2 := &wire.MaxStreamDataFrame{MaximumStreamData: 1338}
-			str.EXPECT().getControlFrame().Return(ackhandler.Frame{Frame: mdf1}, true, true)
-			str.EXPECT().getControlFrame().Return(ackhandler.Frame{Frame: mdf2}, true, false)
-			frames, l := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			now := time.Now()
+			str.EXPECT().getControlFrame(now).Return(ackhandler.Frame{Frame: mdf1}, true, true)
+			str.EXPECT().getControlFrame(now).Return(ackhandler.Frame{Frame: mdf2}, true, false)
+			frames, l := framer.AppendControlFrames(nil, protocol.MaxByteCount, now, protocol.Version1)
 			Expect(frames).To(HaveLen(3))
 			Expect(frames[0].Frame).To(Equal(mdf1))
 			Expect(frames[1].Frame).To(Equal(mdf2))
@@ -90,8 +92,8 @@ var _ = Describe("Framer", func() {
 			str := NewMockStreamControlFrameGetter(mockCtrl)
 			framer.AddStreamWithControlFrames(10, str)
 			mdf1 := &wire.MaxStreamDataFrame{MaximumStreamData: 1337}
-			str.EXPECT().getControlFrame().Return(ackhandler.Frame{Frame: mdf1}, true, true).AnyTimes()
-			frames, l := framer.AppendControlFrames(nil, 100, protocol.Version1)
+			str.EXPECT().getControlFrame(gomock.Any()).Return(ackhandler.Frame{Frame: mdf1}, true, true).AnyTimes()
+			frames, l := framer.AppendControlFrames(nil, 100, time.Now(), protocol.Version1)
 			Expect(l).To(Equal(protocol.ByteCount(len(frames)) * mdf1.Length(protocol.Version1)))
 			Expect(l).To(And(
 				BeNumerically(">", 100-maxStreamControlFrameSize),
@@ -107,10 +109,10 @@ var _ = Describe("Framer", func() {
 			for i := 0; i < numFrames+1; i++ {
 				framer.QueueControlFrame(bf)
 			}
-			frames, length := framer.AppendControlFrames(nil, maxSize, protocol.Version1)
+			frames, length := framer.AppendControlFrames(nil, maxSize, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(numFrames))
 			Expect(length).To(BeNumerically(">", maxSize-bfLen))
-			frames, length = framer.AppendControlFrames(nil, maxSize, protocol.Version1)
+			frames, length = framer.AppendControlFrames(nil, maxSize, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(1))
 			Expect(length).To(Equal(bfLen))
 		})
@@ -133,7 +135,7 @@ var _ = Describe("Framer", func() {
 				framer.QueueControlFrame(f)
 			}
 			framer.Handle0RTTRejection()
-			fs, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			fs, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			Expect(fs).To(HaveLen(2))
 			Expect(length).To(Equal(ping.Length(version) + ncid.Length(version)))
 		})
@@ -143,7 +145,7 @@ var _ = Describe("Framer", func() {
 				framer.QueueControlFrame(&wire.PingFrame{})
 				framer.QueueControlFrame(&wire.PingFrame{})
 				Expect(framer.QueuedTooManyControlFrames()).To(BeFalse())
-				frames, _ := framer.AppendControlFrames([]ackhandler.Frame{}, 1, protocol.Version1)
+				frames, _ := framer.AppendControlFrames([]ackhandler.Frame{}, 1, time.Now(), protocol.Version1)
 				Expect(frames).To(HaveLen(1))
 				Expect(framer.controlFrames).To(HaveLen(i + 1))
 			}
@@ -168,7 +170,7 @@ var _ = Describe("Framer", func() {
 			framer.QueueControlFrame(cf2)
 			// the first packet should contain a single PATH_RESPONSE frame, but all the other control frames
 			Expect(framer.HasData()).To(BeTrue())
-			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(3))
 			Expect(frames[0].Frame).To(Equal(f1))
 			Expect([]wire.Frame{frames[1].Frame, frames[2].Frame}).To(ContainElement(cf1))
@@ -176,7 +178,7 @@ var _ = Describe("Framer", func() {
 			Expect(length).To(Equal(f1.Length(protocol.Version1) + cf1.Length(protocol.Version1) + cf2.Length(protocol.Version1)))
 			// the second packet should contain the other PATH_RESPONSE frame
 			Expect(framer.HasData()).To(BeTrue())
-			frames, length = framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			frames, length = framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			Expect(frames).To(HaveLen(1))
 			Expect(frames[0].Frame).To(Equal(f2))
 			Expect(length).To(Equal(f2.Length(protocol.Version1)))
@@ -193,13 +195,13 @@ var _ = Describe("Framer", func() {
 			}
 			for i := 0; i < maxPathResponses; i++ {
 				Expect(framer.HasData()).To(BeTrue())
-				frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+				frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 				Expect(frames).To(HaveLen(1))
 				Expect(frames[0].Frame).To(Equal(pathResponses[i]))
 				Expect(length).To(Equal(pathResponses[i].Length(protocol.Version1)))
 			}
 			Expect(framer.HasData()).To(BeFalse())
-			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
+			frames, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, time.Now(), protocol.Version1)
 			Expect(frames).To(BeEmpty())
 			Expect(length).To(BeZero())
 		})
diff --git a/http3/conn_test.go b/http3/conn_test.go
index 552a6f4f..7eb33840 100644
--- a/http3/conn_test.go
+++ b/http3/conn_test.go
@@ -257,7 +257,6 @@ var _ = Describe("Connection", func() {
 		})
 
 		for _, pers := range []protocol.Perspective{protocol.PerspectiveServer, protocol.PerspectiveClient} {
-			pers := pers
 			expectedErr := ErrCodeIDError
 			if pers == protocol.PerspectiveClient {
 				expectedErr = ErrCodeStreamCreationError
diff --git a/http3/http3_suite_test.go b/http3/http3_suite_test.go
index b34003b9..53ad1a0f 100644
--- a/http3/http3_suite_test.go
+++ b/http3/http3_suite_test.go
@@ -26,7 +26,6 @@ var _ = AfterEach(func() {
 	mockCtrl.Finish()
 })
 
-//nolint:unparam
 func scaleDuration(t time.Duration) time.Duration {
 	scaleFactor := 1
 	if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
diff --git a/integrationtests/gomodvendor/go.mod b/integrationtests/gomodvendor/go.mod
index 3411b837..f3ddd4e2 100644
--- a/integrationtests/gomodvendor/go.mod
+++ b/integrationtests/gomodvendor/go.mod
@@ -15,11 +15,11 @@ require (
 	go.uber.org/mock v0.4.0 // indirect
 	golang.org/x/crypto v0.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
-	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/net v0.28.0 // indirect
 	golang.org/x/sys v0.23.0 // indirect
 	golang.org/x/text v0.17.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/tools v0.22.0 // indirect
 )
 
 replace github.com/quic-go/quic-go => ../../
diff --git a/integrationtests/gomodvendor/go.sum b/integrationtests/gomodvendor/go.sum
index 8d125919..bf1014b0 100644
--- a/integrationtests/gomodvendor/go.sum
+++ b/integrationtests/gomodvendor/go.sum
@@ -33,8 +33,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
@@ -46,8 +46,8 @@ golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/integrationtests/self/mtu_test.go b/integrationtests/self/mtu_test.go
index 744d9fa0..cd96ad28 100644
--- a/integrationtests/self/mtu_test.go
+++ b/integrationtests/self/mtu_test.go
@@ -72,7 +72,7 @@ var _ = Describe("DPLPMTUD", func() {
 		defer proxy.Close()
 
 		// Make sure to use v4-only socket here.
-		// We can't reliably set the DF bit on dual-stack sockets on macOS.
+		// We can't reliably set the DF bit on dual-stack sockets on macOS before Sequoia (macOS 15).
 		udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
 		Expect(err).ToNot(HaveOccurred())
 		defer udpConn.Close()
diff --git a/internal/flowcontrol/base_flow_controller_test.go b/internal/flowcontrol/base_flow_controller_test.go
deleted file mode 100644
index da6cf357..00000000
--- a/internal/flowcontrol/base_flow_controller_test.go
+++ /dev/null
@@ -1,236 +0,0 @@
-package flowcontrol
-
-import (
-	"os"
-	"strconv"
-	"time"
-
-	"github.com/quic-go/quic-go/internal/protocol"
-	"github.com/quic-go/quic-go/internal/utils"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-)
-
-// on the CIs, the timing is a lot less precise, so scale every duration by this factor
-//
-//nolint:unparam
-func scaleDuration(t time.Duration) time.Duration {
-	scaleFactor := 1
-	if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
-		scaleFactor = f
-	}
-	Expect(scaleFactor).ToNot(BeZero())
-	return time.Duration(scaleFactor) * t
-}
-
-var _ = Describe("Base Flow controller", func() {
-	var controller *baseFlowController
-
-	BeforeEach(func() {
-		controller = &baseFlowController{}
-		controller.rttStats = &utils.RTTStats{}
-	})
-
-	Context("send flow control", func() {
-		It("adds bytes sent", func() {
-			controller.bytesSent = 5
-			controller.AddBytesSent(6)
-			Expect(controller.bytesSent).To(Equal(protocol.ByteCount(5 + 6)))
-		})
-
-		It("gets the size of the remaining flow control window", func() {
-			controller.bytesSent = 5
-			controller.sendWindow = 12
-			Expect(controller.sendWindowSize()).To(Equal(protocol.ByteCount(12 - 5)))
-		})
-
-		It("updates the size of the flow control window", func() {
-			controller.AddBytesSent(5)
-			controller.UpdateSendWindow(15)
-			Expect(controller.sendWindow).To(Equal(protocol.ByteCount(15)))
-			Expect(controller.sendWindowSize()).To(Equal(protocol.ByteCount(15 - 5)))
-		})
-
-		It("says that the window size is 0 if we sent more than we were allowed to", func() {
-			controller.AddBytesSent(15)
-			controller.UpdateSendWindow(10)
-			Expect(controller.sendWindowSize()).To(BeZero())
-		})
-
-		It("does not decrease the flow control window", func() {
-			Expect(controller.UpdateSendWindow(20)).To(BeTrue())
-			Expect(controller.sendWindowSize()).To(Equal(protocol.ByteCount(20)))
-			Expect(controller.UpdateSendWindow(10)).To(BeFalse())
-			Expect(controller.sendWindowSize()).To(Equal(protocol.ByteCount(20)))
-		})
-
-		It("says when it's blocked", func() {
-			controller.UpdateSendWindow(100)
-			Expect(controller.IsNewlyBlocked()).To(BeFalse())
-			controller.AddBytesSent(100)
-			blocked, offset := controller.IsNewlyBlocked()
-			Expect(blocked).To(BeTrue())
-			Expect(offset).To(Equal(protocol.ByteCount(100)))
-		})
-
-		It("doesn't say that it's newly blocked multiple times for the same offset", func() {
-			controller.UpdateSendWindow(100)
-			controller.AddBytesSent(100)
-			newlyBlocked, offset := controller.IsNewlyBlocked()
-			Expect(newlyBlocked).To(BeTrue())
-			Expect(offset).To(Equal(protocol.ByteCount(100)))
-			newlyBlocked, _ = controller.IsNewlyBlocked()
-			Expect(newlyBlocked).To(BeFalse())
-			controller.UpdateSendWindow(150)
-			controller.AddBytesSent(150)
-			newlyBlocked, _ = controller.IsNewlyBlocked()
-			Expect(newlyBlocked).To(BeTrue())
-		})
-	})
-
-	Context("receive flow control", func() {
-		var (
-			receiveWindow     protocol.ByteCount = 10000
-			receiveWindowSize protocol.ByteCount = 1000
-		)
-
-		BeforeEach(func() {
-			controller.bytesRead = receiveWindow - receiveWindowSize
-			controller.receiveWindow = receiveWindow
-			controller.receiveWindowSize = receiveWindowSize
-		})
-
-		It("adds bytes read", func() {
-			controller.bytesRead = 5
-			controller.addBytesRead(6)
-			Expect(controller.bytesRead).To(Equal(protocol.ByteCount(5 + 6)))
-		})
-
-		It("triggers a window update when necessary", func() {
-			bytesConsumed := float64(receiveWindowSize)*protocol.WindowUpdateThreshold + 1 // consumed 1 byte more than the threshold
-			bytesRemaining := receiveWindowSize - protocol.ByteCount(bytesConsumed)
-			readPosition := receiveWindow - bytesRemaining
-			controller.bytesRead = readPosition
-			offset := controller.getWindowUpdate()
-			Expect(offset).To(Equal(readPosition + receiveWindowSize))
-			Expect(controller.receiveWindow).To(Equal(readPosition + receiveWindowSize))
-		})
-
-		It("doesn't trigger a window update when not necessary", func() {
-			bytesConsumed := float64(receiveWindowSize)*protocol.WindowUpdateThreshold - 1 // consumed 1 byte less than the threshold
-			bytesRemaining := receiveWindowSize - protocol.ByteCount(bytesConsumed)
-			readPosition := receiveWindow - bytesRemaining
-			controller.bytesRead = readPosition
-			offset := controller.getWindowUpdate()
-			Expect(offset).To(BeZero())
-		})
-
-		Context("receive window size auto-tuning", func() {
-			var oldWindowSize protocol.ByteCount
-
-			BeforeEach(func() {
-				oldWindowSize = controller.receiveWindowSize
-				controller.maxReceiveWindowSize = 5000
-			})
-
-			// update the congestion such that it returns a given value for the smoothed RTT
-			setRtt := func(t time.Duration) {
-				controller.rttStats.UpdateRTT(t, 0, time.Now())
-				Expect(controller.rttStats.SmoothedRTT()).To(Equal(t)) // make sure it worked
-			}
-
-			It("doesn't increase the window size for a new stream", func() {
-				controller.maybeAdjustWindowSize()
-				Expect(controller.receiveWindowSize).To(Equal(oldWindowSize))
-			})
-
-			It("doesn't increase the window size when no RTT estimate is available", func() {
-				setRtt(0)
-				controller.startNewAutoTuningEpoch(time.Now())
-				controller.addBytesRead(400)
-				offset := controller.getWindowUpdate()
-				Expect(offset).ToNot(BeZero()) // make sure a window update is sent
-				Expect(controller.receiveWindowSize).To(Equal(oldWindowSize))
-			})
-
-			It("increases the window size if read so fast that the window would be consumed in less than 4 RTTs", func() {
-				bytesRead := controller.bytesRead
-				rtt := scaleDuration(50 * time.Millisecond)
-				setRtt(rtt)
-				// consume more than 2/3 of the window...
-				dataRead := receiveWindowSize*2/3 + 1
-				// ... in 4*2/3 of the RTT
-				controller.epochStartOffset = controller.bytesRead
-				controller.epochStartTime = time.Now().Add(-rtt * 4 * 2 / 3)
-				controller.addBytesRead(dataRead)
-				offset := controller.getWindowUpdate()
-				Expect(offset).ToNot(BeZero())
-				// check that the window size was increased
-				newWindowSize := controller.receiveWindowSize
-				Expect(newWindowSize).To(Equal(2 * oldWindowSize))
-				// check that the new window size was used to increase the offset
-				Expect(offset).To(Equal(bytesRead + dataRead + newWindowSize))
-			})
-
-			It("doesn't increase the window size if data is read so fast that the window would be consumed in less than 4 RTTs, but less than half the window has been read", func() {
-				bytesRead := controller.bytesRead
-				rtt := scaleDuration(20 * time.Millisecond)
-				setRtt(rtt)
-				// consume more than 2/3 of the window...
-				dataRead := receiveWindowSize*1/3 + 1
-				// ... in 4*2/3 of the RTT
-				controller.epochStartOffset = controller.bytesRead
-				controller.epochStartTime = time.Now().Add(-rtt * 4 * 1 / 3)
-				controller.addBytesRead(dataRead)
-				offset := controller.getWindowUpdate()
-				Expect(offset).ToNot(BeZero())
-				// check that the window size was not increased
-				newWindowSize := controller.receiveWindowSize
-				Expect(newWindowSize).To(Equal(oldWindowSize))
-				// check that the new window size was used to increase the offset
-				Expect(offset).To(Equal(bytesRead + dataRead + newWindowSize))
-			})
-
-			It("doesn't increase the window size if read too slowly", func() {
-				bytesRead := controller.bytesRead
-				rtt := scaleDuration(20 * time.Millisecond)
-				setRtt(rtt)
-				// consume less than 2/3 of the window...
-				dataRead := receiveWindowSize*2/3 - 1
-				// ... in 4*2/3 of the RTT
-				controller.epochStartOffset = controller.bytesRead
-				controller.epochStartTime = time.Now().Add(-rtt * 4 * 2 / 3)
-				controller.addBytesRead(dataRead)
-				offset := controller.getWindowUpdate()
-				Expect(offset).ToNot(BeZero())
-				// check that the window size was not increased
-				Expect(controller.receiveWindowSize).To(Equal(oldWindowSize))
-				// check that the new window size was used to increase the offset
-				Expect(offset).To(Equal(bytesRead + dataRead + oldWindowSize))
-			})
-
-			It("doesn't increase the window size to a value higher than the maxReceiveWindowSize", func() {
-				resetEpoch := func() {
-					// make sure the next call to maybeAdjustWindowSize will increase the window
-					controller.epochStartTime = time.Now().Add(-time.Millisecond)
-					controller.epochStartOffset = controller.bytesRead
-					controller.addBytesRead(controller.receiveWindowSize/2 + 1)
-				}
-				setRtt(scaleDuration(20 * time.Millisecond))
-				resetEpoch()
-				controller.maybeAdjustWindowSize()
-				Expect(controller.receiveWindowSize).To(Equal(2 * oldWindowSize)) // 2000
-				// because the lastWindowUpdateTime is updated by MaybeTriggerWindowUpdate(), we can just call maybeAdjustWindowSize() multiple times and get an increase of the window size every time
-				resetEpoch()
-				controller.maybeAdjustWindowSize()
-				Expect(controller.receiveWindowSize).To(Equal(2 * 2 * oldWindowSize)) // 4000
-				resetEpoch()
-				controller.maybeAdjustWindowSize()
-				Expect(controller.receiveWindowSize).To(Equal(controller.maxReceiveWindowSize)) // 5000
-				controller.maybeAdjustWindowSize()
-				Expect(controller.receiveWindowSize).To(Equal(controller.maxReceiveWindowSize)) // 5000
-			})
-		})
-	})
-})
diff --git a/internal/flowcontrol/connection_flow_controller_test.go b/internal/flowcontrol/connection_flow_controller_test.go
index 1314db1b..0598d1dc 100644
--- a/internal/flowcontrol/connection_flow_controller_test.go
+++ b/internal/flowcontrol/connection_flow_controller_test.go
@@ -1,178 +1,67 @@
 package flowcontrol
 
 import (
+	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/internal/qerr"
 	"github.com/quic-go/quic-go/internal/utils"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Connection Flow controller", func() {
-	var controller *connectionFlowController
-
-	// update the congestion such that it returns a given value for the smoothed RTT
-	setRtt := func(t time.Duration) {
-		controller.rttStats.UpdateRTT(t, 0, time.Now())
-		Expect(controller.rttStats.SmoothedRTT()).To(Equal(t)) // make sure it worked
-	}
-
-	BeforeEach(func() {
-		controller = &connectionFlowController{}
-		controller.rttStats = &utils.RTTStats{}
-		controller.logger = utils.DefaultLogger
-		controller.allowWindowIncrease = func(protocol.ByteCount) bool { return true }
-	})
-
-	Context("Constructor", func() {
-		rttStats := &utils.RTTStats{}
-
-		It("sets the send and receive windows", func() {
-			receiveWindow := protocol.ByteCount(2000)
-			maxReceiveWindow := protocol.ByteCount(3000)
-
-			fc := NewConnectionFlowController(
-				receiveWindow,
-				maxReceiveWindow,
-				func(protocol.ByteCount) bool { return true },
-				rttStats,
-				utils.DefaultLogger).(*connectionFlowController)
-			Expect(fc.receiveWindow).To(Equal(receiveWindow))
-			Expect(fc.maxReceiveWindowSize).To(Equal(maxReceiveWindow))
-		})
-	})
-
-	Context("receive flow control", func() {
-		It("increases the highestReceived by a given window size", func() {
-			controller.highestReceived = 1337
-			controller.IncrementHighestReceived(123)
-			Expect(controller.highestReceived).To(Equal(protocol.ByteCount(1337 + 123)))
-		})
-
-		Context("getting window updates", func() {
-			BeforeEach(func() {
-				controller.receiveWindow = 100
-				controller.receiveWindowSize = 60
-				controller.maxReceiveWindowSize = 1000
-				controller.bytesRead = 100 - 60
-			})
-
-			It("queues window updates", func() {
-				controller.AddBytesRead(1)
-				Expect(controller.GetWindowUpdate()).To(BeZero())
-				controller.AddBytesRead(29)
-				Expect(controller.GetWindowUpdate()).ToNot(BeZero())
-				controller.AddBytesRead(1)
-				Expect(controller.GetWindowUpdate()).To(BeZero())
-			})
-
-			It("gets a window update", func() {
-				windowSize := controller.receiveWindowSize
-				oldOffset := controller.bytesRead
-				dataRead := windowSize/2 - 1 // make sure not to trigger auto-tuning
-				controller.AddBytesRead(dataRead)
-				offset := controller.GetWindowUpdate()
-				Expect(offset).To(Equal(oldOffset + dataRead + 60))
-			})
-
-			It("auto-tunes the window", func() {
-				var allowed protocol.ByteCount
-				controller.allowWindowIncrease = func(size protocol.ByteCount) bool {
-					allowed = size
-					return true
-				}
-				oldOffset := controller.bytesRead
-				oldWindowSize := controller.receiveWindowSize
-				rtt := scaleDuration(20 * time.Millisecond)
-				setRtt(rtt)
-				controller.epochStartTime = time.Now().Add(-time.Millisecond)
-				controller.epochStartOffset = oldOffset
-				dataRead := oldWindowSize/2 + 1
-				controller.AddBytesRead(dataRead)
-				offset := controller.GetWindowUpdate()
-				newWindowSize := controller.receiveWindowSize
-				Expect(newWindowSize).To(Equal(2 * oldWindowSize))
-				Expect(offset).To(Equal(oldOffset + dataRead + newWindowSize))
-				Expect(allowed).To(Equal(oldWindowSize))
-			})
-
-			It("doesn't auto-tune the window if it's not allowed", func() {
-				controller.allowWindowIncrease = func(protocol.ByteCount) bool { return false }
-				oldOffset := controller.bytesRead
-				oldWindowSize := controller.receiveWindowSize
-				rtt := scaleDuration(20 * time.Millisecond)
-				setRtt(rtt)
-				controller.epochStartTime = time.Now().Add(-time.Millisecond)
-				controller.epochStartOffset = oldOffset
-				dataRead := oldWindowSize/2 + 1
-				controller.AddBytesRead(dataRead)
-				offset := controller.GetWindowUpdate()
-				newWindowSize := controller.receiveWindowSize
-				Expect(newWindowSize).To(Equal(oldWindowSize))
-				Expect(offset).To(Equal(oldOffset + dataRead + newWindowSize))
-			})
-		})
-	})
-
-	Context("setting the minimum window size", func() {
-		var (
-			oldWindowSize     protocol.ByteCount
-			receiveWindow     protocol.ByteCount = 10000
-			receiveWindowSize protocol.ByteCount = 1000
-		)
-
-		BeforeEach(func() {
-			controller.receiveWindow = receiveWindow
-			controller.receiveWindowSize = receiveWindowSize
-			oldWindowSize = controller.receiveWindowSize
-			controller.maxReceiveWindowSize = 3000
-		})
-
-		It("sets the minimum window window size", func() {
-			controller.EnsureMinimumWindowSize(1800)
-			Expect(controller.receiveWindowSize).To(Equal(protocol.ByteCount(1800)))
-		})
-
-		It("doesn't reduce the window window size", func() {
-			controller.EnsureMinimumWindowSize(1)
-			Expect(controller.receiveWindowSize).To(Equal(oldWindowSize))
-		})
-
-		It("doesn't increase the window size beyond the maxReceiveWindowSize", func() {
-			max := controller.maxReceiveWindowSize
-			controller.EnsureMinimumWindowSize(2 * max)
-			Expect(controller.receiveWindowSize).To(Equal(max))
-		})
-
-		It("starts a new epoch after the window size was increased", func() {
-			controller.EnsureMinimumWindowSize(1912)
-			Expect(controller.epochStartTime).To(BeTemporally("~", time.Now(), 100*time.Millisecond))
-		})
-	})
-
-	Context("resetting", func() {
-		It("resets", func() {
-			const initialWindow protocol.ByteCount = 1337
-			controller.UpdateSendWindow(initialWindow)
-			controller.AddBytesSent(1000)
-			Expect(controller.SendWindowSize()).To(Equal(initialWindow - 1000))
-			Expect(controller.Reset()).To(Succeed())
-			Expect(controller.SendWindowSize()).To(Equal(initialWindow))
-		})
-
-		It("says if is blocked after resetting", func() {
-			const initialWindow protocol.ByteCount = 1337
-			controller.UpdateSendWindow(initialWindow)
-			controller.AddBytesSent(initialWindow)
-			blocked, _ := controller.IsNewlyBlocked()
-			Expect(blocked).To(BeTrue())
-			Expect(controller.Reset()).To(Succeed())
-			controller.AddBytesSent(initialWindow)
-			blocked, blockedAt := controller.IsNewlyBlocked()
-			Expect(blocked).To(BeTrue())
-			Expect(blockedAt).To(Equal(initialWindow))
-		})
-	})
-})
+func TestConnectionFlowControlWindowUpdate(t *testing.T) {
+	fc := NewConnectionFlowController(
+		100, // initial receive window
+		100, // max receive window
+		nil,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+	fc.AddBytesRead(100)
+	require.Equal(t, protocol.ByteCount(200), fc.GetWindowUpdate(time.Now()))
+}
+
+func TestConnectionWindowAutoTuningNotAllowed(t *testing.T) {
+	// the RTT is 1 second
+	rttStats := &utils.RTTStats{}
+	rttStats.UpdateRTT(time.Second, 0, time.Now())
+	require.Equal(t, time.Second, rttStats.SmoothedRTT())
+
+	callbackCalledWith := protocol.InvalidByteCount
+	fc := NewConnectionFlowController(
+		100, // initial receive window
+		150, // max receive window
+		func(size protocol.ByteCount) bool {
+			callbackCalledWith = size
+			return false
+		},
+		rttStats,
+		utils.DefaultLogger,
+	)
+	now := time.Now()
+	require.NoError(t, fc.IncrementHighestReceived(100, now))
+	fc.AddBytesRead(90)
+	require.Equal(t, protocol.InvalidByteCount, callbackCalledWith)
+	require.Equal(t, protocol.ByteCount(90+100), fc.GetWindowUpdate(now.Add(time.Millisecond)))
+	require.Equal(t, protocol.ByteCount(150-100), callbackCalledWith)
+}
+
+func TestConnectionFlowControlViolation(t *testing.T) {
+	fc := NewConnectionFlowController(100, 100, nil, &utils.RTTStats{}, utils.DefaultLogger)
+	require.NoError(t, fc.IncrementHighestReceived(40, time.Now()))
+	require.NoError(t, fc.IncrementHighestReceived(60, time.Now()))
+	err := fc.IncrementHighestReceived(1, time.Now())
+	var terr *qerr.TransportError
+	require.ErrorAs(t, err, &terr)
+	require.Equal(t, qerr.FlowControlError, terr.ErrorCode)
+}
+
+// TODO (#4732): add a test for successfully resetting the flow controller
+func TestConnectionFlowControllerReset(t *testing.T) {
+	fc := NewConnectionFlowController(0, 0, nil, &utils.RTTStats{}, utils.DefaultLogger)
+	fc.AddBytesRead(1)
+	require.EqualError(t, fc.Reset(), "flow controller reset after reading data")
+}
diff --git a/internal/flowcontrol/flowcontrol_suite_test.go b/internal/flowcontrol/flowcontrol_suite_test.go
deleted file mode 100644
index 6cfe981d..00000000
--- a/internal/flowcontrol/flowcontrol_suite_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package flowcontrol
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-	"go.uber.org/mock/gomock"
-)
-
-func TestFlowControl(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "FlowControl Suite")
-}
-
-var mockCtrl *gomock.Controller
-
-var _ = BeforeEach(func() {
-	mockCtrl = gomock.NewController(GinkgoT())
-})
-
-var _ = AfterEach(func() {
-	mockCtrl.Finish()
-})
diff --git a/internal/flowcontrol/stream_flow_controller_test.go b/internal/flowcontrol/stream_flow_controller_test.go
index 83e1ad12..da3e78df 100644
--- a/internal/flowcontrol/stream_flow_controller_test.go
+++ b/internal/flowcontrol/stream_flow_controller_test.go
@@ -1,262 +1,265 @@
 package flowcontrol
 
 import (
+	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 	"github.com/quic-go/quic-go/internal/utils"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Stream Flow controller", func() {
-	var controller *streamFlowController
-
-	BeforeEach(func() {
-		rttStats := &utils.RTTStats{}
-		controller = &streamFlowController{
-			streamID: 10,
-			connection: NewConnectionFlowController(
-				1000,
-				1000,
-				func(protocol.ByteCount) bool { return true },
-				rttStats,
+func TestStreamFlowControlReceiving(t *testing.T) {
+	fc := NewStreamFlowController(
+		42,
+		NewConnectionFlowController(
+			protocol.MaxByteCount,
+			protocol.MaxByteCount,
+			nil,
+			&utils.RTTStats{},
+			utils.DefaultLogger,
+		),
+		100,
+		protocol.MaxByteCount,
+		protocol.MaxByteCount,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+
+	require.NoError(t, fc.UpdateHighestReceived(50, false, time.Now()))
+	// duplicates are fine
+	require.NoError(t, fc.UpdateHighestReceived(50, false, time.Now()))
+	// reordering is fine
+	require.NoError(t, fc.UpdateHighestReceived(40, false, time.Now()))
+	require.NoError(t, fc.UpdateHighestReceived(60, false, time.Now()))
+
+	// exceeding the limit is not fine
+	err := fc.UpdateHighestReceived(101, false, time.Now())
+	var terr *qerr.TransportError
+	require.ErrorAs(t, err, &terr)
+	require.Equal(t, qerr.FlowControlError, terr.ErrorCode)
+	require.Equal(t, "received 101 bytes on stream 42, allowed 100 bytes", terr.ErrorMessage)
+}
+
+func TestStreamFlowControllerFinalOffset(t *testing.T) {
+	newFC := func() StreamFlowController {
+		return NewStreamFlowController(
+			42,
+			NewConnectionFlowController(
+				protocol.MaxByteCount,
+				protocol.MaxByteCount,
+				nil,
+				&utils.RTTStats{},
 				utils.DefaultLogger,
-			).(*connectionFlowController),
-		}
-		controller.maxReceiveWindowSize = 10000
-		controller.rttStats = rttStats
-		controller.logger = utils.DefaultLogger
+			),
+			protocol.MaxByteCount,
+			protocol.MaxByteCount,
+			protocol.MaxByteCount,
+			&utils.RTTStats{},
+			utils.DefaultLogger,
+		)
+	}
+
+	t.Run("duplicate final offset", func(t *testing.T) {
+		fc := newFC()
+		require.NoError(t, fc.UpdateHighestReceived(50, true, time.Now()))
+		// it is valid to receive the same final offset multiple times
+		require.NoError(t, fc.UpdateHighestReceived(50, true, time.Now()))
 	})
 
-	Context("Constructor", func() {
-		rttStats := &utils.RTTStats{}
-		const receiveWindow protocol.ByteCount = 2000
-		const maxReceiveWindow protocol.ByteCount = 3000
-		const sendWindow protocol.ByteCount = 4000
-
-		It("sets the send and receive windows", func() {
-			cc := NewConnectionFlowController(0, 0, func(protocol.ByteCount) bool { return true }, nil, utils.DefaultLogger)
-			fc := NewStreamFlowController(5, cc, receiveWindow, maxReceiveWindow, sendWindow, rttStats, utils.DefaultLogger).(*streamFlowController)
-			Expect(fc.streamID).To(Equal(protocol.StreamID(5)))
-			Expect(fc.receiveWindow).To(Equal(receiveWindow))
-			Expect(fc.maxReceiveWindowSize).To(Equal(maxReceiveWindow))
-			Expect(fc.sendWindow).To(Equal(sendWindow))
-		})
-
-		It("queues window updates", func() {
-			cc := NewConnectionFlowController(receiveWindow, maxReceiveWindow, func(protocol.ByteCount) bool { return true }, nil, utils.DefaultLogger)
-			fc := NewStreamFlowController(5, cc, receiveWindow, maxReceiveWindow, sendWindow, rttStats, utils.DefaultLogger).(*streamFlowController)
-			Expect(fc.AddBytesRead(receiveWindow)).To(BeTrue())
-		})
+	t.Run("inconsistent final offset", func(t *testing.T) {
+		fc := newFC()
+		require.NoError(t, fc.UpdateHighestReceived(50, true, time.Now()))
+		err := fc.UpdateHighestReceived(51, true, time.Now())
+		require.Error(t, err)
+		var terr *qerr.TransportError
+		require.ErrorAs(t, err, &terr)
+		require.Equal(t, qerr.FinalSizeError, terr.ErrorCode)
+		require.Equal(t, "received inconsistent final offset for stream 42 (old: 50, new: 51 bytes)", terr.ErrorMessage)
 	})
 
-	Context("receiving data", func() {
-		Context("registering received offsets", func() {
-			var receiveWindow protocol.ByteCount = 10000
-			var receiveWindowSize protocol.ByteCount = 600
-
-			BeforeEach(func() {
-				controller.receiveWindow = receiveWindow
-				controller.receiveWindowSize = receiveWindowSize
-			})
-
-			It("updates the highestReceived", func() {
-				controller.highestReceived = 1337
-				Expect(controller.UpdateHighestReceived(1338, false)).To(Succeed())
-				Expect(controller.highestReceived).To(Equal(protocol.ByteCount(1338)))
-			})
-
-			It("informs the connection flow controller about received data", func() {
-				controller.highestReceived = 10
-				controller.connection.(*connectionFlowController).highestReceived = 100
-				Expect(controller.UpdateHighestReceived(20, false)).To(Succeed())
-				Expect(controller.connection.(*connectionFlowController).highestReceived).To(Equal(protocol.ByteCount(100 + 10)))
-			})
-
-			It("does not decrease the highestReceived", func() {
-				controller.highestReceived = 1337
-				Expect(controller.UpdateHighestReceived(1000, false)).To(Succeed())
-				Expect(controller.highestReceived).To(Equal(protocol.ByteCount(1337)))
-			})
-
-			It("does nothing when setting the same byte offset", func() {
-				controller.highestReceived = 1337
-				Expect(controller.UpdateHighestReceived(1337, false)).To(Succeed())
-			})
-
-			It("does not give a flow control violation when using the window completely", func() {
-				controller.connection.(*connectionFlowController).receiveWindow = receiveWindow
-				Expect(controller.UpdateHighestReceived(receiveWindow, false)).To(Succeed())
-			})
-
-			It("detects a flow control violation", func() {
-				Expect(controller.UpdateHighestReceived(receiveWindow+1, false)).To(MatchError(&qerr.TransportError{
-					ErrorCode:    qerr.FlowControlError,
-					ErrorMessage: "received 10001 bytes on stream 10, allowed 10000 bytes",
-				}))
-			})
-
-			It("accepts a final offset higher than the highest received", func() {
-				Expect(controller.UpdateHighestReceived(100, false)).To(Succeed())
-				Expect(controller.UpdateHighestReceived(101, true)).To(Succeed())
-				Expect(controller.highestReceived).To(Equal(protocol.ByteCount(101)))
-			})
-
-			It("errors when receiving a final offset smaller than the highest offset received so far", func() {
-				controller.UpdateHighestReceived(100, false)
-				Expect(controller.UpdateHighestReceived(50, true)).To(MatchError(&qerr.TransportError{
-					ErrorCode:    qerr.FinalSizeError,
-					ErrorMessage: "received final offset 50 for stream 10, but already received offset 100 before",
-				}))
-			})
-
-			It("accepts delayed data after receiving a final offset", func() {
-				Expect(controller.UpdateHighestReceived(300, true)).To(Succeed())
-				Expect(controller.UpdateHighestReceived(250, false)).To(Succeed())
-			})
-
-			It("errors when receiving a higher offset after receiving a final offset", func() {
-				Expect(controller.UpdateHighestReceived(200, true)).To(Succeed())
-				Expect(controller.UpdateHighestReceived(250, false)).To(MatchError(&qerr.TransportError{
-					ErrorCode:    qerr.FinalSizeError,
-					ErrorMessage: "received offset 250 for stream 10, but final offset was already received at 200",
-				}))
-			})
-
-			It("accepts duplicate final offsets", func() {
-				Expect(controller.UpdateHighestReceived(200, true)).To(Succeed())
-				Expect(controller.UpdateHighestReceived(200, true)).To(Succeed())
-				Expect(controller.highestReceived).To(Equal(protocol.ByteCount(200)))
-			})
-
-			It("errors when receiving inconsistent final offsets", func() {
-				Expect(controller.UpdateHighestReceived(200, true)).To(Succeed())
-				Expect(controller.UpdateHighestReceived(201, true)).To(MatchError(&qerr.TransportError{
-					ErrorCode:    qerr.FinalSizeError,
-					ErrorMessage: "received inconsistent final offset for stream 10 (old: 200, new: 201 bytes)",
-				}))
-			})
-
-			It("tells the connection flow controller when a stream is abandoned", func() {
-				controller.AddBytesRead(5)
-				Expect(controller.UpdateHighestReceived(100, true)).To(Succeed())
-				controller.Abandon()
-				Expect(controller.connection.(*connectionFlowController).bytesRead).To(Equal(protocol.ByteCount(100)))
-			})
-
-			It("tolerates repeated calls to Abandon", func() {
-				controller.AddBytesRead(5)
-				Expect(controller.UpdateHighestReceived(100, true)).To(Succeed())
-				controller.Abandon()
-				controller.Abandon()
-				controller.Abandon()
-				Expect(controller.connection.(*connectionFlowController).bytesRead).To(Equal(protocol.ByteCount(100)))
-			})
-		})
-
-		It("saves when data is read", func() {
-			controller.AddBytesRead(200)
-			Expect(controller.bytesRead).To(Equal(protocol.ByteCount(200)))
-			Expect(controller.connection.(*connectionFlowController).bytesRead).To(Equal(protocol.ByteCount(200)))
-		})
-
-		Context("generating window updates", func() {
-			var oldWindowSize protocol.ByteCount
-
-			// update the congestion such that it returns a given value for the smoothed RTT
-			setRtt := func(t time.Duration) {
-				controller.rttStats.UpdateRTT(t, 0, time.Now())
-				Expect(controller.rttStats.SmoothedRTT()).To(Equal(t)) // make sure it worked
-			}
-
-			BeforeEach(func() {
-				controller.receiveWindow = 100
-				controller.receiveWindowSize = 60
-				controller.bytesRead = 100 - 60
-				controller.connection.(*connectionFlowController).receiveWindow = 100
-				controller.connection.(*connectionFlowController).receiveWindowSize = 120
-				oldWindowSize = controller.receiveWindowSize
-			})
-
-			It("queues window updates", func() {
-				Expect(controller.AddBytesRead(1)).To(BeFalse())
-				Expect(controller.AddBytesRead(29)).To(BeTrue())
-				Expect(controller.GetWindowUpdate()).ToNot(BeZero())
-				Expect(controller.AddBytesRead(1)).To(BeFalse())
-			})
-
-			It("tells the connection flow controller when the window was auto-tuned", func() {
-				var allowed protocol.ByteCount
-				controller.connection.(*connectionFlowController).allowWindowIncrease = func(size protocol.ByteCount) bool {
-					allowed = size
-					return true
-				}
-				oldOffset := controller.bytesRead
-				setRtt(scaleDuration(20 * time.Millisecond))
-				controller.epochStartOffset = oldOffset
-				controller.epochStartTime = time.Now().Add(-time.Millisecond)
-				controller.AddBytesRead(55)
-				offset := controller.GetWindowUpdate()
-				Expect(offset).To(Equal(oldOffset + 55 + 2*oldWindowSize))
-				Expect(controller.receiveWindowSize).To(Equal(2 * oldWindowSize))
-				Expect(allowed).To(Equal(oldWindowSize))
-				Expect(controller.connection.(*connectionFlowController).receiveWindowSize).To(Equal(protocol.ByteCount(float64(controller.receiveWindowSize) * protocol.ConnectionFlowControlMultiplier)))
-			})
-
-			It("doesn't increase the connection flow control window if it's not allowed", func() {
-				oldOffset := controller.bytesRead
-				oldConnectionSize := controller.connection.(*connectionFlowController).receiveWindowSize
-				controller.connection.(*connectionFlowController).allowWindowIncrease = func(protocol.ByteCount) bool { return false }
-				setRtt(scaleDuration(20 * time.Millisecond))
-				controller.epochStartOffset = oldOffset
-				controller.epochStartTime = time.Now().Add(-time.Millisecond)
-				controller.AddBytesRead(55)
-				offset := controller.GetWindowUpdate()
-				Expect(offset).To(Equal(oldOffset + 55 + 2*oldWindowSize))
-				Expect(controller.receiveWindowSize).To(Equal(2 * oldWindowSize))
-				Expect(controller.connection.(*connectionFlowController).receiveWindowSize).To(Equal(oldConnectionSize))
-			})
-
-			It("sends a connection-level window update when a large stream is abandoned", func() {
-				Expect(controller.UpdateHighestReceived(90, true)).To(Succeed())
-				Expect(controller.connection.GetWindowUpdate()).To(BeZero())
-				controller.Abandon()
-				Expect(controller.connection.GetWindowUpdate()).ToNot(BeZero())
-			})
-
-			It("doesn't increase the window after a final offset was already received", func() {
-				Expect(controller.UpdateHighestReceived(90, true)).To(Succeed())
-				Expect(controller.AddBytesRead(30)).To(BeFalse())
-				Expect(controller.GetWindowUpdate()).To(BeZero())
-			})
-		})
+	t.Run("non-final offset past final offset", func(t *testing.T) {
+		fc := newFC()
+		require.NoError(t, fc.UpdateHighestReceived(50, true, time.Now()))
+		// No matter the ordering, it's never ok to receive an offset past the final offset.
+		err := fc.UpdateHighestReceived(60, false, time.Now())
+		var terr *qerr.TransportError
+		require.ErrorAs(t, err, &terr)
+		require.Equal(t, qerr.FinalSizeError, terr.ErrorCode)
+		require.Equal(t, "received offset 60 for stream 42, but final offset was already received at 50", terr.ErrorMessage)
 	})
 
-	Context("sending data", func() {
-		It("gets the size of the send window", func() {
-			controller.connection.UpdateSendWindow(1000)
-			controller.UpdateSendWindow(15)
-			controller.AddBytesSent(5)
-			Expect(controller.SendWindowSize()).To(Equal(protocol.ByteCount(10)))
-		})
-
-		It("makes sure that it doesn't overflow the connection-level window", func() {
-			controller.connection.UpdateSendWindow(12)
-			controller.UpdateSendWindow(20)
-			controller.AddBytesSent(10)
-			Expect(controller.SendWindowSize()).To(Equal(protocol.ByteCount(2)))
-		})
-
-		It("doesn't say that it's blocked, if only the connection is blocked", func() {
-			controller.connection.UpdateSendWindow(50)
-			controller.UpdateSendWindow(100)
-			controller.AddBytesSent(50)
-			blocked, _ := controller.connection.IsNewlyBlocked()
-			Expect(blocked).To(BeTrue())
-			Expect(controller.IsNewlyBlocked()).To(BeFalse())
-		})
+	t.Run("final offset smaller than previous offset", func(t *testing.T) {
+		fc := newFC()
+		require.NoError(t, fc.UpdateHighestReceived(50, false, time.Now()))
+		// If we received offset already, it's invalid to receive a smaller final offset.
+		err := fc.UpdateHighestReceived(40, true, time.Now())
+		var terr *qerr.TransportError
+		require.ErrorAs(t, err, &terr)
+		require.Equal(t, qerr.FinalSizeError, terr.ErrorCode)
+		require.Equal(t, "received final offset 40 for stream 42, but already received offset 50 before", terr.ErrorMessage)
 	})
-})
+}
+
+func TestStreamAbandoning(t *testing.T) {
+	connFC := NewConnectionFlowController(
+		100,
+		protocol.MaxByteCount,
+		nil,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+	require.True(t, connFC.UpdateSendWindow(300))
+	fc := NewStreamFlowController(
+		42,
+		connFC,
+		60,
+		protocol.MaxByteCount,
+		100,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+
+	require.NoError(t, fc.UpdateHighestReceived(50, true, time.Now()))
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+	require.Zero(t, connFC.GetWindowUpdate(time.Now()))
+
+	// Abandon the stream.
+	// This marks all bytes as having been consumed.
+	fc.Abandon()
+	require.Equal(t, protocol.ByteCount(150), connFC.GetWindowUpdate(time.Now()))
+}
+
+func TestStreamSendWindow(t *testing.T) {
+	// We set up the connection flow controller with a limit of 300 bytes,
+	// and the stream flow controller with a limit of 100 bytes.
+	connFC := NewConnectionFlowController(
+		protocol.MaxByteCount,
+		protocol.MaxByteCount,
+		nil,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+	require.True(t, connFC.UpdateSendWindow(300))
+	fc := NewStreamFlowController(
+		42,
+		connFC,
+		protocol.MaxByteCount,
+		protocol.MaxByteCount,
+		100,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+	// first, we're limited by the stream flow controller
+	require.Equal(t, protocol.ByteCount(100), fc.SendWindowSize())
+	fc.AddBytesSent(50)
+	require.False(t, fc.IsNewlyBlocked())
+	require.Equal(t, protocol.ByteCount(50), fc.SendWindowSize())
+	fc.AddBytesSent(50)
+	require.True(t, fc.IsNewlyBlocked())
+	require.Zero(t, fc.SendWindowSize())
+	require.False(t, fc.IsNewlyBlocked()) // we're still blocked, but it's not new
+
+	// Update the stream flow control limit, but don't update the connection flow control limit.
+	// We're now limited by the connection flow controller.
+	require.True(t, fc.UpdateSendWindow(1000))
+	// reordered updates are ignored
+	require.False(t, fc.UpdateSendWindow(999))
+
+	require.False(t, fc.IsNewlyBlocked()) // we're not blocked anymore
+	require.Equal(t, protocol.ByteCount(200), fc.SendWindowSize())
+	fc.AddBytesSent(200)
+	require.Zero(t, fc.SendWindowSize())
+	require.False(t, fc.IsNewlyBlocked()) // we're blocked, but not on stream flow control
+}
+
+func TestStreamWindowUpdate(t *testing.T) {
+	fc := NewStreamFlowController(
+		42,
+		NewConnectionFlowController(
+			protocol.MaxByteCount,
+			protocol.MaxByteCount,
+			nil,
+			&utils.RTTStats{},
+			utils.DefaultLogger,
+		),
+		100,
+		100,
+		protocol.MaxByteCount,
+		&utils.RTTStats{},
+		utils.DefaultLogger,
+	)
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+	fc.AddBytesRead(24)
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+	// the window is updated when it's 25% filled
+	fc.AddBytesRead(1)
+	require.Equal(t, protocol.ByteCount(125), fc.GetWindowUpdate(time.Now()))
+
+	fc.AddBytesRead(24)
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+	// the window is updated when it's 25% filled
+	fc.AddBytesRead(1)
+	require.Equal(t, protocol.ByteCount(150), fc.GetWindowUpdate(time.Now()))
+
+	// Receive the final offset.
+	// We don't need to send any more flow control updates.
+	require.NoError(t, fc.UpdateHighestReceived(100, true, time.Now()))
+	fc.AddBytesRead(50)
+	require.Zero(t, fc.GetWindowUpdate(time.Now()))
+}
+
+func TestStreamWindowAutoTuning(t *testing.T) {
+	// the RTT is 1 second
+	rttStats := &utils.RTTStats{}
+	rttStats.UpdateRTT(time.Second, 0, time.Now())
+	require.Equal(t, time.Second, rttStats.SmoothedRTT())
+
+	connFC := NewConnectionFlowController(
+		150, // initial receive window
+		350, // max receive window
+		func(size protocol.ByteCount) bool { return true },
+		rttStats,
+		utils.DefaultLogger,
+	)
+	fc := NewStreamFlowController(
+		42,
+		connFC,
+		100, // initial send window
+		399, // max send window
+		protocol.MaxByteCount,
+		rttStats,
+		utils.DefaultLogger,
+	)
+
+	now := time.Now()
+	require.NoError(t, fc.UpdateHighestReceived(100, false, now))
+
+	// data consumption is too slow, window size is not increased
+	now = now.Add(2500 * time.Millisecond)
+	fc.AddBytesRead(51)
+	// one initial stream window size added
+	require.Equal(t, protocol.ByteCount(51+100), fc.GetWindowUpdate(now))
+	// one initial connection window size added
+	require.Equal(t, protocol.ByteCount(51+150), connFC.getWindowUpdate(now))
+
+	// data consumption is fast enough, window size is increased
+	now = now.Add(2 * time.Second)
+	fc.AddBytesRead(51)
+	// stream window size doubled to 200 bytes
+	require.Equal(t, protocol.ByteCount(102+2*100), fc.GetWindowUpdate(now))
+	// The connection window is now increased as well,
+	// so that we don't get blocked on connection level flow control:
+	// The increase is by 200 bytes * a connection factor of 1.5: 300 bytes.
+	require.Equal(t, protocol.ByteCount(102+300), connFC.GetWindowUpdate(now))
+
+	// data consumption is fast enough, window size is increased
+	now = now.Add(2 * time.Second)
+	fc.AddBytesRead(101)
+	// stream window size increased again, but bumps into its maximum value
+	require.Equal(t, protocol.ByteCount(203+399), fc.GetWindowUpdate(now))
+	// the connection window is also increased, but it bumps into its maximum value
+	require.Equal(t, protocol.ByteCount(203+350), connFC.GetWindowUpdate(now))
+}
diff --git a/internal/handshake/updatable_aead_test.go b/internal/handshake/updatable_aead_test.go
index b5053f02..b95ae5a3 100644
--- a/internal/handshake/updatable_aead_test.go
+++ b/internal/handshake/updatable_aead_test.go
@@ -329,7 +329,6 @@ func TestRejectFrequentKeyUpdates(t *testing.T) {
 	}, err)
 }
 
-//nolint:unparam
 func setKeyUpdateIntervals(t *testing.T, firstKeyUpdateInterval, keyUpdateInterval uint64) {
 	origKeyUpdateInterval := KeyUpdateInterval
 	origFirstKeyUpdateInterval := FirstKeyUpdateInterval
diff --git a/internal/qtls/qtls_test.go b/internal/qtls/qtls_test.go
index b201fc8e..28f887b1 100644
--- a/internal/qtls/qtls_test.go
+++ b/internal/qtls/qtls_test.go
@@ -109,7 +109,6 @@ func TestServerConfigGetConfigForClientRecursively(t *testing.T) {
 	var localAddr, remoteAddr net.Addr
 	tlsConf := &tls.Config{}
 	var innerConf *tls.Config
-	//nolint:unparam
 	getCert := func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
 		localAddr = info.Conn.LocalAddr()
 		remoteAddr = info.Conn.RemoteAddr()
diff --git a/logging/connection_tracer_test.go b/logging/connection_tracer_test.go
index fa79f6a5..95229869 100644
--- a/logging/connection_tracer_test.go
+++ b/logging/connection_tracer_test.go
@@ -2,349 +2,20 @@ package logging_test
 
 import (
 	"errors"
-	"net"
 	"testing"
-	"time"
 
-	mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
-	"github.com/quic-go/quic-go/internal/protocol"
-	"github.com/quic-go/quic-go/internal/wire"
 	"github.com/quic-go/quic-go/logging"
-
-	"go.uber.org/mock/gomock"
+	"github.com/stretchr/testify/require"
 )
 
-func TestConnectionTracerStartedConnection(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	local := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4)}
-	remote := &net.UDPAddr{IP: net.IPv4(4, 3, 2, 1)}
-	dest := protocol.ParseConnectionID([]byte{1, 2, 3, 4})
-	src := protocol.ParseConnectionID([]byte{4, 3, 2, 1})
-	tr1.EXPECT().StartedConnection(local, remote, src, dest)
-	tr2.EXPECT().StartedConnection(local, remote, src, dest)
-	tracer.StartedConnection(local, remote, src, dest)
-}
-
-func TestConnectionTracerNegotiatedVersion(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	chosen := protocol.Version2
-	client := []protocol.Version{protocol.Version1}
-	server := []protocol.Version{13, 37}
-	tr1.EXPECT().NegotiatedVersion(chosen, client, server)
-	tr2.EXPECT().NegotiatedVersion(chosen, client, server)
-	tracer.NegotiatedVersion(chosen, client, server)
-}
-
-func TestConnectionTracerClosedConnection(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
+func TestConnectionTracerMultiplexing(t *testing.T) {
+	var err1, err2 error
+	t1 := &logging.ConnectionTracer{ClosedConnection: func(e error) { err1 = e }}
+	t2 := &logging.ConnectionTracer{ClosedConnection: func(e error) { err2 = e }}
 	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
 
 	e := errors.New("test err")
-	tr1.EXPECT().ClosedConnection(e)
-	tr2.EXPECT().ClosedConnection(e)
 	tracer.ClosedConnection(e)
-}
-
-func TestConnectionTracerSentTransportParameters(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tp := &wire.TransportParameters{InitialMaxData: 1337}
-	tr1.EXPECT().SentTransportParameters(tp)
-	tr2.EXPECT().SentTransportParameters(tp)
-	tracer.SentTransportParameters(tp)
-}
-
-func TestConnectionTracerReceivedTransportParameters(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tp := &wire.TransportParameters{InitialMaxData: 1337}
-	tr1.EXPECT().ReceivedTransportParameters(tp)
-	tr2.EXPECT().ReceivedTransportParameters(tp)
-	tracer.ReceivedTransportParameters(tp)
-}
-
-func TestConnectionTracerRestoredTransportParameters(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tp := &wire.TransportParameters{InitialMaxData: 1337}
-	tr1.EXPECT().RestoredTransportParameters(tp)
-	tr2.EXPECT().RestoredTransportParameters(tp)
-	tracer.RestoredTransportParameters(tp)
-}
-
-func TestConnectionTracerSentLongHeaderPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	hdr := &logging.ExtendedHeader{Header: logging.Header{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}}
-	ack := &logging.AckFrame{AckRanges: []logging.AckRange{{Smallest: 1, Largest: 10}}}
-	ping := &logging.PingFrame{}
-	tr1.EXPECT().SentLongHeaderPacket(hdr, logging.ByteCount(1337), logging.ECTNot, ack, []logging.Frame{ping})
-	tr2.EXPECT().SentLongHeaderPacket(hdr, logging.ByteCount(1337), logging.ECTNot, ack, []logging.Frame{ping})
-	tracer.SentLongHeaderPacket(hdr, 1337, logging.ECTNot, ack, []logging.Frame{ping})
-}
-
-func TestConnectionTracerSentShortHeaderPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	hdr := &logging.ShortHeader{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}
-	ack := &logging.AckFrame{AckRanges: []logging.AckRange{{Smallest: 1, Largest: 10}}}
-	ping := &logging.PingFrame{}
-	tr1.EXPECT().SentShortHeaderPacket(hdr, logging.ByteCount(1337), logging.ECNCE, ack, []logging.Frame{ping})
-	tr2.EXPECT().SentShortHeaderPacket(hdr, logging.ByteCount(1337), logging.ECNCE, ack, []logging.Frame{ping})
-	tracer.SentShortHeaderPacket(hdr, 1337, logging.ECNCE, ack, []logging.Frame{ping})
-}
-
-func TestConnectionTracerReceivedVersionNegotiationPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	src := logging.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
-	dest := logging.ArbitraryLenConnectionID{1, 2, 3, 4}
-	tr1.EXPECT().ReceivedVersionNegotiationPacket(dest, src, []logging.Version{1337})
-	tr2.EXPECT().ReceivedVersionNegotiationPacket(dest, src, []logging.Version{1337})
-	tracer.ReceivedVersionNegotiationPacket(dest, src, []logging.Version{1337})
-}
-
-func TestConnectionTracerReceivedRetry(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	hdr := &logging.Header{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}
-	tr1.EXPECT().ReceivedRetry(hdr)
-	tr2.EXPECT().ReceivedRetry(hdr)
-	tracer.ReceivedRetry(hdr)
-}
-
-func TestConnectionTracerReceivedLongHeaderPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	hdr := &logging.ExtendedHeader{Header: logging.Header{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}}
-	ping := &logging.PingFrame{}
-	tr1.EXPECT().ReceivedLongHeaderPacket(hdr, logging.ByteCount(1337), logging.ECT1, []logging.Frame{ping})
-	tr2.EXPECT().ReceivedLongHeaderPacket(hdr, logging.ByteCount(1337), logging.ECT1, []logging.Frame{ping})
-	tracer.ReceivedLongHeaderPacket(hdr, 1337, logging.ECT1, []logging.Frame{ping})
-}
-
-func TestConnectionTracerReceivedShortHeaderPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	hdr := &logging.ShortHeader{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}
-	ping := &logging.PingFrame{}
-	tr1.EXPECT().ReceivedShortHeaderPacket(hdr, logging.ByteCount(1337), logging.ECT0, []logging.Frame{ping})
-	tr2.EXPECT().ReceivedShortHeaderPacket(hdr, logging.ByteCount(1337), logging.ECT0, []logging.Frame{ping})
-	tracer.ReceivedShortHeaderPacket(hdr, 1337, logging.ECT0, []logging.Frame{ping})
-}
-
-func TestConnectionTracerBufferedPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().BufferedPacket(logging.PacketTypeHandshake, logging.ByteCount(1337))
-	tr2.EXPECT().BufferedPacket(logging.PacketTypeHandshake, logging.ByteCount(1337))
-	tracer.BufferedPacket(logging.PacketTypeHandshake, 1337)
-}
-
-func TestConnectionTracerDroppedPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().DroppedPacket(logging.PacketTypeInitial, logging.PacketNumber(42), logging.ByteCount(1337), logging.PacketDropHeaderParseError)
-	tr2.EXPECT().DroppedPacket(logging.PacketTypeInitial, logging.PacketNumber(42), logging.ByteCount(1337), logging.PacketDropHeaderParseError)
-	tracer.DroppedPacket(logging.PacketTypeInitial, 42, 1337, logging.PacketDropHeaderParseError)
-}
-
-func TestConnectionTracerUpdatedMTU(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().UpdatedMTU(logging.ByteCount(1337), true)
-	tr2.EXPECT().UpdatedMTU(logging.ByteCount(1337), true)
-	tracer.UpdatedMTU(1337, true)
-}
-
-func TestConnectionTracerUpdatedCongestionState(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().UpdatedCongestionState(logging.CongestionStateRecovery)
-	tr2.EXPECT().UpdatedCongestionState(logging.CongestionStateRecovery)
-	tracer.UpdatedCongestionState(logging.CongestionStateRecovery)
-}
-
-func TestConnectionTracerUpdatedMetrics(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	rttStats := &logging.RTTStats{}
-	rttStats.UpdateRTT(time.Second, 0, time.Now())
-	tr1.EXPECT().UpdatedMetrics(rttStats, logging.ByteCount(1337), logging.ByteCount(42), 13)
-	tr2.EXPECT().UpdatedMetrics(rttStats, logging.ByteCount(1337), logging.ByteCount(42), 13)
-	tracer.UpdatedMetrics(rttStats, 1337, 42, 13)
-}
-
-func TestConnectionTracerAcknowledgedPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().AcknowledgedPacket(logging.EncryptionHandshake, logging.PacketNumber(42))
-	tr2.EXPECT().AcknowledgedPacket(logging.EncryptionHandshake, logging.PacketNumber(42))
-	tracer.AcknowledgedPacket(logging.EncryptionHandshake, 42)
-}
-
-func TestConnectionTracerLostPacket(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().LostPacket(logging.EncryptionHandshake, logging.PacketNumber(42), logging.PacketLossReorderingThreshold)
-	tr2.EXPECT().LostPacket(logging.EncryptionHandshake, logging.PacketNumber(42), logging.PacketLossReorderingThreshold)
-	tracer.LostPacket(logging.EncryptionHandshake, 42, logging.PacketLossReorderingThreshold)
-}
-
-func TestConnectionTracerUpdatedPTOCount(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().UpdatedPTOCount(uint32(88))
-	tr2.EXPECT().UpdatedPTOCount(uint32(88))
-	tracer.UpdatedPTOCount(88)
-}
-
-func TestConnectionTracerUpdatedKeyFromTLS(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().UpdatedKeyFromTLS(logging.EncryptionHandshake, logging.PerspectiveClient)
-	tr2.EXPECT().UpdatedKeyFromTLS(logging.EncryptionHandshake, logging.PerspectiveClient)
-	tracer.UpdatedKeyFromTLS(logging.EncryptionHandshake, logging.PerspectiveClient)
-}
-
-func TestConnectionTracerUpdatedKey(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().UpdatedKey(logging.KeyPhase(42), true)
-	tr2.EXPECT().UpdatedKey(logging.KeyPhase(42), true)
-	tracer.UpdatedKey(logging.KeyPhase(42), true)
-}
-
-func TestConnectionTracerDroppedEncryptionLevel(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().DroppedEncryptionLevel(logging.EncryptionHandshake)
-	tr2.EXPECT().DroppedEncryptionLevel(logging.EncryptionHandshake)
-	tracer.DroppedEncryptionLevel(logging.EncryptionHandshake)
-}
-
-func TestConnectionTracerDroppedKey(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().DroppedKey(logging.KeyPhase(123))
-	tr2.EXPECT().DroppedKey(logging.KeyPhase(123))
-	tracer.DroppedKey(123)
-}
-
-func TestConnectionTracerSetLossTimer(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	now := time.Now()
-	tr1.EXPECT().SetLossTimer(logging.TimerTypePTO, logging.EncryptionHandshake, now)
-	tr2.EXPECT().SetLossTimer(logging.TimerTypePTO, logging.EncryptionHandshake, now)
-	tracer.SetLossTimer(logging.TimerTypePTO, logging.EncryptionHandshake, now)
-}
-
-func TestConnectionTracerLossTimerExpired(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().LossTimerExpired(logging.TimerTypePTO, logging.EncryptionHandshake)
-	tr2.EXPECT().LossTimerExpired(logging.TimerTypePTO, logging.EncryptionHandshake)
-	tracer.LossTimerExpired(logging.TimerTypePTO, logging.EncryptionHandshake)
-}
-
-func TestConnectionTracerLossTimerCanceled(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().LossTimerCanceled()
-	tr2.EXPECT().LossTimerCanceled()
-	tracer.LossTimerCanceled()
-}
-
-func TestConnectionTracerClose(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	t1, tr1 := mocklogging.NewMockConnectionTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockConnectionTracer(ctrl)
-	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
-
-	tr1.EXPECT().Close()
-	tr2.EXPECT().Close()
-	tracer.Close()
+	require.Equal(t, e, err1)
+	require.Equal(t, e, err2)
 }
diff --git a/logging/tracer_test.go b/logging/tracer_test.go
index c3641e38..b1fbc39b 100644
--- a/logging/tracer_test.go
+++ b/logging/tracer_test.go
@@ -4,12 +4,10 @@ import (
 	"net"
 	"testing"
 
-	mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
 	"github.com/quic-go/quic-go/internal/protocol"
 	. "github.com/quic-go/quic-go/logging"
 
 	"github.com/stretchr/testify/require"
-	"go.uber.org/mock/gomock"
 )
 
 func TestNilTracerWhenEmpty(t *testing.T) {
@@ -23,69 +21,16 @@ func TestSingleTracer(t *testing.T) {
 }
 
 func TestTracerPacketSent(t *testing.T) {
-	ctrl := gomock.NewController(t)
-
-	t1, tr1 := mocklogging.NewMockTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockTracer(ctrl)
+	var s1, s2 ByteCount
+	t1 := &Tracer{SentPacket: func(_ net.Addr, _ *Header, s ByteCount, _ []Frame) { s1 = s }}
+	t2 := &Tracer{SentPacket: func(_ net.Addr, _ *Header, s ByteCount, _ []Frame) { s2 = s }}
 	tracer := NewMultiplexedTracer(t1, t2, &Tracer{})
 
+	const size ByteCount = 1024
 	remote := &net.UDPAddr{IP: net.IPv4(4, 3, 2, 1)}
 	hdr := &Header{DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3})}
 	f := &MaxDataFrame{MaximumData: 1337}
-	tr1.EXPECT().SentPacket(remote, hdr, ByteCount(1024), []Frame{f})
-	tr2.EXPECT().SentPacket(remote, hdr, ByteCount(1024), []Frame{f})
-	tracer.SentPacket(remote, hdr, 1024, []Frame{f})
-}
-
-func TestTracerVersionNegotiationSent(t *testing.T) {
-	ctrl := gomock.NewController(t)
-
-	t1, tr1 := mocklogging.NewMockTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockTracer(ctrl)
-	tracer := NewMultiplexedTracer(t1, t2, &Tracer{})
-
-	remote := &net.UDPAddr{IP: net.IPv4(4, 3, 2, 1)}
-	src := ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
-	dest := ArbitraryLenConnectionID{1, 2, 3, 4}
-	versions := []Version{1, 2, 3}
-	tr1.EXPECT().SentVersionNegotiationPacket(remote, dest, src, versions)
-	tr2.EXPECT().SentVersionNegotiationPacket(remote, dest, src, versions)
-	tracer.SentVersionNegotiationPacket(remote, dest, src, versions)
-}
-
-func TestTracerPacketDropped(t *testing.T) {
-	ctrl := gomock.NewController(t)
-
-	t1, tr1 := mocklogging.NewMockTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockTracer(ctrl)
-	tracer := NewMultiplexedTracer(t1, t2, &Tracer{})
-
-	remote := &net.UDPAddr{IP: net.IPv4(4, 3, 2, 1)}
-	tr1.EXPECT().DroppedPacket(remote, PacketTypeRetry, ByteCount(1024), PacketDropDuplicate)
-	tr2.EXPECT().DroppedPacket(remote, PacketTypeRetry, ByteCount(1024), PacketDropDuplicate)
-	tracer.DroppedPacket(remote, PacketTypeRetry, 1024, PacketDropDuplicate)
-}
-
-func TestTracerDebug(t *testing.T) {
-	ctrl := gomock.NewController(t)
-
-	t1, tr1 := mocklogging.NewMockTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockTracer(ctrl)
-	tracer := NewMultiplexedTracer(t1, t2, &Tracer{})
-
-	tr1.EXPECT().Debug("foo", "bar")
-	tr2.EXPECT().Debug("foo", "bar")
-	tracer.Debug("foo", "bar")
-}
-
-func TestTracerClose(t *testing.T) {
-	ctrl := gomock.NewController(t)
-
-	t1, tr1 := mocklogging.NewMockTracer(ctrl)
-	t2, tr2 := mocklogging.NewMockTracer(ctrl)
-	tracer := NewMultiplexedTracer(t1, t2, &Tracer{})
-
-	tr1.EXPECT().Close()
-	tr2.EXPECT().Close()
-	tracer.Close()
+	tracer.SentPacket(remote, hdr, size, []Frame{f})
+	require.Equal(t, size, s1)
+	require.Equal(t, size, s2)
 }
diff --git a/mock_frame_source_test.go b/mock_frame_source_test.go
index 9aa8be71..1d53829f 100644
--- a/mock_frame_source_test.go
+++ b/mock_frame_source_test.go
@@ -11,6 +11,7 @@ package quic
 
 import (
 	reflect "reflect"
+	time "time"
 
 	ackhandler "github.com/quic-go/quic-go/internal/ackhandler"
 	protocol "github.com/quic-go/quic-go/internal/protocol"
@@ -41,18 +42,18 @@ func (m *MockFrameSource) EXPECT() *MockFrameSourceMockRecorder {
 }
 
 // AppendControlFrames mocks base method.
-func (m *MockFrameSource) AppendControlFrames(arg0 []ackhandler.Frame, arg1 protocol.ByteCount, arg2 protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
+func (m *MockFrameSource) AppendControlFrames(arg0 []ackhandler.Frame, arg1 protocol.ByteCount, arg2 time.Time, arg3 protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "AppendControlFrames", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "AppendControlFrames", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].([]ackhandler.Frame)
 	ret1, _ := ret[1].(protocol.ByteCount)
 	return ret0, ret1
 }
 
 // AppendControlFrames indicates an expected call of AppendControlFrames.
-func (mr *MockFrameSourceMockRecorder) AppendControlFrames(arg0, arg1, arg2 any) *MockFrameSourceAppendControlFramesCall {
+func (mr *MockFrameSourceMockRecorder) AppendControlFrames(arg0, arg1, arg2, arg3 any) *MockFrameSourceAppendControlFramesCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendControlFrames", reflect.TypeOf((*MockFrameSource)(nil).AppendControlFrames), arg0, arg1, arg2)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendControlFrames", reflect.TypeOf((*MockFrameSource)(nil).AppendControlFrames), arg0, arg1, arg2, arg3)
 	return &MockFrameSourceAppendControlFramesCall{Call: call}
 }
 
@@ -68,13 +69,13 @@ func (c *MockFrameSourceAppendControlFramesCall) Return(arg0 []ackhandler.Frame,
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockFrameSourceAppendControlFramesCall) Do(f func([]ackhandler.Frame, protocol.ByteCount, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)) *MockFrameSourceAppendControlFramesCall {
+func (c *MockFrameSourceAppendControlFramesCall) Do(f func([]ackhandler.Frame, protocol.ByteCount, time.Time, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)) *MockFrameSourceAppendControlFramesCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockFrameSourceAppendControlFramesCall) DoAndReturn(f func([]ackhandler.Frame, protocol.ByteCount, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)) *MockFrameSourceAppendControlFramesCall {
+func (c *MockFrameSourceAppendControlFramesCall) DoAndReturn(f func([]ackhandler.Frame, protocol.ByteCount, time.Time, protocol.Version) ([]ackhandler.Frame, protocol.ByteCount)) *MockFrameSourceAppendControlFramesCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/mock_packer_test.go b/mock_packer_test.go
index 1d9ec1cc..e8a2d49c 100644
--- a/mock_packer_test.go
+++ b/mock_packer_test.go
@@ -11,6 +11,7 @@ package quic
 
 import (
 	reflect "reflect"
+	time "time"
 
 	ackhandler "github.com/quic-go/quic-go/internal/ackhandler"
 	protocol "github.com/quic-go/quic-go/internal/protocol"
@@ -42,18 +43,18 @@ func (m *MockPacker) EXPECT() *MockPackerMockRecorder {
 }
 
 // AppendPacket mocks base method.
-func (m *MockPacker) AppendPacket(arg0 *packetBuffer, arg1 protocol.ByteCount, arg2 protocol.Version) (shortHeaderPacket, error) {
+func (m *MockPacker) AppendPacket(arg0 *packetBuffer, arg1 protocol.ByteCount, arg2 time.Time, arg3 protocol.Version) (shortHeaderPacket, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "AppendPacket", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "AppendPacket", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(shortHeaderPacket)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // AppendPacket indicates an expected call of AppendPacket.
-func (mr *MockPackerMockRecorder) AppendPacket(arg0, arg1, arg2 any) *MockPackerAppendPacketCall {
+func (mr *MockPackerMockRecorder) AppendPacket(arg0, arg1, arg2, arg3 any) *MockPackerAppendPacketCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendPacket", reflect.TypeOf((*MockPacker)(nil).AppendPacket), arg0, arg1, arg2)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendPacket", reflect.TypeOf((*MockPacker)(nil).AppendPacket), arg0, arg1, arg2, arg3)
 	return &MockPackerAppendPacketCall{Call: call}
 }
 
@@ -69,30 +70,30 @@ func (c *MockPackerAppendPacketCall) Return(arg0 shortHeaderPacket, arg1 error)
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockPackerAppendPacketCall) Do(f func(*packetBuffer, protocol.ByteCount, protocol.Version) (shortHeaderPacket, error)) *MockPackerAppendPacketCall {
+func (c *MockPackerAppendPacketCall) Do(f func(*packetBuffer, protocol.ByteCount, time.Time, protocol.Version) (shortHeaderPacket, error)) *MockPackerAppendPacketCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockPackerAppendPacketCall) DoAndReturn(f func(*packetBuffer, protocol.ByteCount, protocol.Version) (shortHeaderPacket, error)) *MockPackerAppendPacketCall {
+func (c *MockPackerAppendPacketCall) DoAndReturn(f func(*packetBuffer, protocol.ByteCount, time.Time, protocol.Version) (shortHeaderPacket, error)) *MockPackerAppendPacketCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
 
 // MaybePackProbePacket mocks base method.
-func (m *MockPacker) MaybePackProbePacket(arg0 protocol.EncryptionLevel, arg1 protocol.ByteCount, arg2 protocol.Version) (*coalescedPacket, error) {
+func (m *MockPacker) MaybePackProbePacket(arg0 protocol.EncryptionLevel, arg1 protocol.ByteCount, arg2 time.Time, arg3 protocol.Version) (*coalescedPacket, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "MaybePackProbePacket", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "MaybePackProbePacket", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(*coalescedPacket)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // MaybePackProbePacket indicates an expected call of MaybePackProbePacket.
-func (mr *MockPackerMockRecorder) MaybePackProbePacket(arg0, arg1, arg2 any) *MockPackerMaybePackProbePacketCall {
+func (mr *MockPackerMockRecorder) MaybePackProbePacket(arg0, arg1, arg2, arg3 any) *MockPackerMaybePackProbePacketCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaybePackProbePacket", reflect.TypeOf((*MockPacker)(nil).MaybePackProbePacket), arg0, arg1, arg2)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaybePackProbePacket", reflect.TypeOf((*MockPacker)(nil).MaybePackProbePacket), arg0, arg1, arg2, arg3)
 	return &MockPackerMaybePackProbePacketCall{Call: call}
 }
 
@@ -108,21 +109,21 @@ func (c *MockPackerMaybePackProbePacketCall) Return(arg0 *coalescedPacket, arg1
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockPackerMaybePackProbePacketCall) Do(f func(protocol.EncryptionLevel, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)) *MockPackerMaybePackProbePacketCall {
+func (c *MockPackerMaybePackProbePacketCall) Do(f func(protocol.EncryptionLevel, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error)) *MockPackerMaybePackProbePacketCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockPackerMaybePackProbePacketCall) DoAndReturn(f func(protocol.EncryptionLevel, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)) *MockPackerMaybePackProbePacketCall {
+func (c *MockPackerMaybePackProbePacketCall) DoAndReturn(f func(protocol.EncryptionLevel, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error)) *MockPackerMaybePackProbePacketCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
 
 // PackAckOnlyPacket mocks base method.
-func (m *MockPacker) PackAckOnlyPacket(arg0 protocol.ByteCount, arg1 protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
+func (m *MockPacker) PackAckOnlyPacket(arg0 protocol.ByteCount, arg1 time.Time, arg2 protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "PackAckOnlyPacket", arg0, arg1)
+	ret := m.ctrl.Call(m, "PackAckOnlyPacket", arg0, arg1, arg2)
 	ret0, _ := ret[0].(shortHeaderPacket)
 	ret1, _ := ret[1].(*packetBuffer)
 	ret2, _ := ret[2].(error)
@@ -130,9 +131,9 @@ func (m *MockPacker) PackAckOnlyPacket(arg0 protocol.ByteCount, arg1 protocol.Ve
 }
 
 // PackAckOnlyPacket indicates an expected call of PackAckOnlyPacket.
-func (mr *MockPackerMockRecorder) PackAckOnlyPacket(arg0, arg1 any) *MockPackerPackAckOnlyPacketCall {
+func (mr *MockPackerMockRecorder) PackAckOnlyPacket(arg0, arg1, arg2 any) *MockPackerPackAckOnlyPacketCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackAckOnlyPacket", reflect.TypeOf((*MockPacker)(nil).PackAckOnlyPacket), arg0, arg1)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackAckOnlyPacket", reflect.TypeOf((*MockPacker)(nil).PackAckOnlyPacket), arg0, arg1, arg2)
 	return &MockPackerPackAckOnlyPacketCall{Call: call}
 }
 
@@ -148,13 +149,13 @@ func (c *MockPackerPackAckOnlyPacketCall) Return(arg0 shortHeaderPacket, arg1 *p
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockPackerPackAckOnlyPacketCall) Do(f func(protocol.ByteCount, protocol.Version) (shortHeaderPacket, *packetBuffer, error)) *MockPackerPackAckOnlyPacketCall {
+func (c *MockPackerPackAckOnlyPacketCall) Do(f func(protocol.ByteCount, time.Time, protocol.Version) (shortHeaderPacket, *packetBuffer, error)) *MockPackerPackAckOnlyPacketCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockPackerPackAckOnlyPacketCall) DoAndReturn(f func(protocol.ByteCount, protocol.Version) (shortHeaderPacket, *packetBuffer, error)) *MockPackerPackAckOnlyPacketCall {
+func (c *MockPackerPackAckOnlyPacketCall) DoAndReturn(f func(protocol.ByteCount, time.Time, protocol.Version) (shortHeaderPacket, *packetBuffer, error)) *MockPackerPackAckOnlyPacketCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -199,18 +200,18 @@ func (c *MockPackerPackApplicationCloseCall) DoAndReturn(f func(*qerr.Applicatio
 }
 
 // PackCoalescedPacket mocks base method.
-func (m *MockPacker) PackCoalescedPacket(arg0 bool, arg1 protocol.ByteCount, arg2 protocol.Version) (*coalescedPacket, error) {
+func (m *MockPacker) PackCoalescedPacket(arg0 bool, arg1 protocol.ByteCount, arg2 time.Time, arg3 protocol.Version) (*coalescedPacket, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "PackCoalescedPacket", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "PackCoalescedPacket", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(*coalescedPacket)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // PackCoalescedPacket indicates an expected call of PackCoalescedPacket.
-func (mr *MockPackerMockRecorder) PackCoalescedPacket(arg0, arg1, arg2 any) *MockPackerPackCoalescedPacketCall {
+func (mr *MockPackerMockRecorder) PackCoalescedPacket(arg0, arg1, arg2, arg3 any) *MockPackerPackCoalescedPacketCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackCoalescedPacket", reflect.TypeOf((*MockPacker)(nil).PackCoalescedPacket), arg0, arg1, arg2)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackCoalescedPacket", reflect.TypeOf((*MockPacker)(nil).PackCoalescedPacket), arg0, arg1, arg2, arg3)
 	return &MockPackerPackCoalescedPacketCall{Call: call}
 }
 
@@ -226,13 +227,13 @@ func (c *MockPackerPackCoalescedPacketCall) Return(arg0 *coalescedPacket, arg1 e
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockPackerPackCoalescedPacketCall) Do(f func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)) *MockPackerPackCoalescedPacketCall {
+func (c *MockPackerPackCoalescedPacketCall) Do(f func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error)) *MockPackerPackCoalescedPacketCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockPackerPackCoalescedPacketCall) DoAndReturn(f func(bool, protocol.ByteCount, protocol.Version) (*coalescedPacket, error)) *MockPackerPackCoalescedPacketCall {
+func (c *MockPackerPackCoalescedPacketCall) DoAndReturn(f func(bool, protocol.ByteCount, time.Time, protocol.Version) (*coalescedPacket, error)) *MockPackerPackCoalescedPacketCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/mock_receive_stream_internal_test.go b/mock_receive_stream_internal_test.go
index 793cdfe0..5204b511 100644
--- a/mock_receive_stream_internal_test.go
+++ b/mock_receive_stream_internal_test.go
@@ -230,17 +230,17 @@ func (c *MockReceiveStreamIcloseForShutdownCall) DoAndReturn(f func(error)) *Moc
 }
 
 // handleResetStreamFrame mocks base method.
-func (m *MockReceiveStreamI) handleResetStreamFrame(arg0 *wire.ResetStreamFrame) error {
+func (m *MockReceiveStreamI) handleResetStreamFrame(arg0 *wire.ResetStreamFrame, arg1 time.Time) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleResetStreamFrame", arg0)
+	ret := m.ctrl.Call(m, "handleResetStreamFrame", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // handleResetStreamFrame indicates an expected call of handleResetStreamFrame.
-func (mr *MockReceiveStreamIMockRecorder) handleResetStreamFrame(arg0 any) *MockReceiveStreamIhandleResetStreamFrameCall {
+func (mr *MockReceiveStreamIMockRecorder) handleResetStreamFrame(arg0, arg1 any) *MockReceiveStreamIhandleResetStreamFrameCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleResetStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleResetStreamFrame), arg0)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleResetStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleResetStreamFrame), arg0, arg1)
 	return &MockReceiveStreamIhandleResetStreamFrameCall{Call: call}
 }
 
@@ -256,29 +256,29 @@ func (c *MockReceiveStreamIhandleResetStreamFrameCall) Return(arg0 error) *MockR
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIhandleResetStreamFrameCall) Do(f func(*wire.ResetStreamFrame) error) *MockReceiveStreamIhandleResetStreamFrameCall {
+func (c *MockReceiveStreamIhandleResetStreamFrameCall) Do(f func(*wire.ResetStreamFrame, time.Time) error) *MockReceiveStreamIhandleResetStreamFrameCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIhandleResetStreamFrameCall) DoAndReturn(f func(*wire.ResetStreamFrame) error) *MockReceiveStreamIhandleResetStreamFrameCall {
+func (c *MockReceiveStreamIhandleResetStreamFrameCall) DoAndReturn(f func(*wire.ResetStreamFrame, time.Time) error) *MockReceiveStreamIhandleResetStreamFrameCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
 
 // handleStreamFrame mocks base method.
-func (m *MockReceiveStreamI) handleStreamFrame(arg0 *wire.StreamFrame) error {
+func (m *MockReceiveStreamI) handleStreamFrame(arg0 *wire.StreamFrame, arg1 time.Time) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleStreamFrame", arg0)
+	ret := m.ctrl.Call(m, "handleStreamFrame", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // handleStreamFrame indicates an expected call of handleStreamFrame.
-func (mr *MockReceiveStreamIMockRecorder) handleStreamFrame(arg0 any) *MockReceiveStreamIhandleStreamFrameCall {
+func (mr *MockReceiveStreamIMockRecorder) handleStreamFrame(arg0, arg1 any) *MockReceiveStreamIhandleStreamFrameCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleStreamFrame), arg0)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleStreamFrame), arg0, arg1)
 	return &MockReceiveStreamIhandleStreamFrameCall{Call: call}
 }
 
@@ -294,13 +294,13 @@ func (c *MockReceiveStreamIhandleStreamFrameCall) Return(arg0 error) *MockReceiv
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIhandleStreamFrameCall) Do(f func(*wire.StreamFrame) error) *MockReceiveStreamIhandleStreamFrameCall {
+func (c *MockReceiveStreamIhandleStreamFrameCall) Do(f func(*wire.StreamFrame, time.Time) error) *MockReceiveStreamIhandleStreamFrameCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIhandleStreamFrameCall) DoAndReturn(f func(*wire.StreamFrame) error) *MockReceiveStreamIhandleStreamFrameCall {
+func (c *MockReceiveStreamIhandleStreamFrameCall) DoAndReturn(f func(*wire.StreamFrame, time.Time) error) *MockReceiveStreamIhandleStreamFrameCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/mock_stream_control_frame_getter_test.go b/mock_stream_control_frame_getter_test.go
index 2fad6e7c..90e85d73 100644
--- a/mock_stream_control_frame_getter_test.go
+++ b/mock_stream_control_frame_getter_test.go
@@ -11,6 +11,7 @@ package quic
 
 import (
 	reflect "reflect"
+	time "time"
 
 	ackhandler "github.com/quic-go/quic-go/internal/ackhandler"
 	gomock "go.uber.org/mock/gomock"
@@ -40,9 +41,9 @@ func (m *MockStreamControlFrameGetter) EXPECT() *MockStreamControlFrameGetterMoc
 }
 
 // getControlFrame mocks base method.
-func (m *MockStreamControlFrameGetter) getControlFrame() (ackhandler.Frame, bool, bool) {
+func (m *MockStreamControlFrameGetter) getControlFrame(arg0 time.Time) (ackhandler.Frame, bool, bool) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "getControlFrame")
+	ret := m.ctrl.Call(m, "getControlFrame", arg0)
 	ret0, _ := ret[0].(ackhandler.Frame)
 	ret1, _ := ret[1].(bool)
 	ret2, _ := ret[2].(bool)
@@ -50,9 +51,9 @@ func (m *MockStreamControlFrameGetter) getControlFrame() (ackhandler.Frame, bool
 }
 
 // getControlFrame indicates an expected call of getControlFrame.
-func (mr *MockStreamControlFrameGetterMockRecorder) getControlFrame() *MockStreamControlFrameGettergetControlFrameCall {
+func (mr *MockStreamControlFrameGetterMockRecorder) getControlFrame(arg0 any) *MockStreamControlFrameGettergetControlFrameCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getControlFrame", reflect.TypeOf((*MockStreamControlFrameGetter)(nil).getControlFrame))
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getControlFrame", reflect.TypeOf((*MockStreamControlFrameGetter)(nil).getControlFrame), arg0)
 	return &MockStreamControlFrameGettergetControlFrameCall{Call: call}
 }
 
@@ -68,13 +69,13 @@ func (c *MockStreamControlFrameGettergetControlFrameCall) Return(arg0 ackhandler
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamControlFrameGettergetControlFrameCall) Do(f func() (ackhandler.Frame, bool, bool)) *MockStreamControlFrameGettergetControlFrameCall {
+func (c *MockStreamControlFrameGettergetControlFrameCall) Do(f func(time.Time) (ackhandler.Frame, bool, bool)) *MockStreamControlFrameGettergetControlFrameCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamControlFrameGettergetControlFrameCall) DoAndReturn(f func() (ackhandler.Frame, bool, bool)) *MockStreamControlFrameGettergetControlFrameCall {
+func (c *MockStreamControlFrameGettergetControlFrameCall) DoAndReturn(f func(time.Time) (ackhandler.Frame, bool, bool)) *MockStreamControlFrameGettergetControlFrameCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/mock_stream_internal_test.go b/mock_stream_internal_test.go
index a27fbed8..757f4ec8 100644
--- a/mock_stream_internal_test.go
+++ b/mock_stream_internal_test.go
@@ -459,17 +459,17 @@ func (c *MockStreamIcloseForShutdownCall) DoAndReturn(f func(error)) *MockStream
 }
 
 // handleResetStreamFrame mocks base method.
-func (m *MockStreamI) handleResetStreamFrame(arg0 *wire.ResetStreamFrame) error {
+func (m *MockStreamI) handleResetStreamFrame(arg0 *wire.ResetStreamFrame, arg1 time.Time) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleResetStreamFrame", arg0)
+	ret := m.ctrl.Call(m, "handleResetStreamFrame", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // handleResetStreamFrame indicates an expected call of handleResetStreamFrame.
-func (mr *MockStreamIMockRecorder) handleResetStreamFrame(arg0 any) *MockStreamIhandleResetStreamFrameCall {
+func (mr *MockStreamIMockRecorder) handleResetStreamFrame(arg0, arg1 any) *MockStreamIhandleResetStreamFrameCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleResetStreamFrame", reflect.TypeOf((*MockStreamI)(nil).handleResetStreamFrame), arg0)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleResetStreamFrame", reflect.TypeOf((*MockStreamI)(nil).handleResetStreamFrame), arg0, arg1)
 	return &MockStreamIhandleResetStreamFrameCall{Call: call}
 }
 
@@ -485,13 +485,13 @@ func (c *MockStreamIhandleResetStreamFrameCall) Return(arg0 error) *MockStreamIh
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamIhandleResetStreamFrameCall) Do(f func(*wire.ResetStreamFrame) error) *MockStreamIhandleResetStreamFrameCall {
+func (c *MockStreamIhandleResetStreamFrameCall) Do(f func(*wire.ResetStreamFrame, time.Time) error) *MockStreamIhandleResetStreamFrameCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamIhandleResetStreamFrameCall) DoAndReturn(f func(*wire.ResetStreamFrame) error) *MockStreamIhandleResetStreamFrameCall {
+func (c *MockStreamIhandleResetStreamFrameCall) DoAndReturn(f func(*wire.ResetStreamFrame, time.Time) error) *MockStreamIhandleResetStreamFrameCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -533,17 +533,17 @@ func (c *MockStreamIhandleStopSendingFrameCall) DoAndReturn(f func(*wire.StopSen
 }
 
 // handleStreamFrame mocks base method.
-func (m *MockStreamI) handleStreamFrame(arg0 *wire.StreamFrame) error {
+func (m *MockStreamI) handleStreamFrame(arg0 *wire.StreamFrame, arg1 time.Time) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleStreamFrame", arg0)
+	ret := m.ctrl.Call(m, "handleStreamFrame", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // handleStreamFrame indicates an expected call of handleStreamFrame.
-func (mr *MockStreamIMockRecorder) handleStreamFrame(arg0 any) *MockStreamIhandleStreamFrameCall {
+func (mr *MockStreamIMockRecorder) handleStreamFrame(arg0, arg1 any) *MockStreamIhandleStreamFrameCall {
 	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleStreamFrame", reflect.TypeOf((*MockStreamI)(nil).handleStreamFrame), arg0)
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleStreamFrame", reflect.TypeOf((*MockStreamI)(nil).handleStreamFrame), arg0, arg1)
 	return &MockStreamIhandleStreamFrameCall{Call: call}
 }
 
@@ -559,13 +559,13 @@ func (c *MockStreamIhandleStreamFrameCall) Return(arg0 error) *MockStreamIhandle
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamIhandleStreamFrameCall) Do(f func(*wire.StreamFrame) error) *MockStreamIhandleStreamFrameCall {
+func (c *MockStreamIhandleStreamFrameCall) Do(f func(*wire.StreamFrame, time.Time) error) *MockStreamIhandleStreamFrameCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamIhandleStreamFrameCall) DoAndReturn(f func(*wire.StreamFrame) error) *MockStreamIhandleStreamFrameCall {
+func (c *MockStreamIhandleStreamFrameCall) DoAndReturn(f func(*wire.StreamFrame, time.Time) error) *MockStreamIhandleStreamFrameCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/packet_packer_test.go b/packet_packer_test.go
index 1dc2494d..16d1b4a8 100644
--- a/packet_packer_test.go
+++ b/packet_packer_test.go
@@ -72,7 +72,7 @@ var _ = Describe("Packet packer", func() {
 	}
 
 	expectAppendControlFrames := func(frames ...ackhandler.Frame) {
-		framer.EXPECT().AppendControlFrames(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(fs []ackhandler.Frame, _ protocol.ByteCount, v protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
+		framer.EXPECT().AppendControlFrames(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(fs []ackhandler.Frame, _ protocol.ByteCount, _ time.Time, v protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
 			var length protocol.ByteCount
 			for _, f := range frames {
 				length += f.Frame.Length(v)
@@ -149,7 +149,7 @@ var _ = Describe("Packet packer", func() {
 			expectAppendControlFrames()
 			f := &wire.StreamFrame{Data: []byte{0xde, 0xca, 0xfb, 0xad}}
 			expectAppendStreamFrames(ackhandler.StreamFrame{Frame: f})
-			p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+			p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(p).ToNot(BeNil())
 			Expect(p.longHdrPackets).To(BeEmpty())
@@ -188,7 +188,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, true)
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, true)
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true)
-				p, err := packer.PackCoalescedPacket(true, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(true, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).To(BeNil())
 			})
@@ -200,7 +200,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().GetInitialSealer().Return(getSealer(), nil)
 				ack := &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 10}}}
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, true).Return(ack)
-				p, err := packer.PackCoalescedPacket(true, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(true, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.longHdrPackets).To(HaveLen(1))
@@ -217,7 +217,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().GetInitialSealer().Return(getSealer(), nil)
 				ack := &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 10}}}
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, true).Return(ack)
-				p, err := packer.PackCoalescedPacket(true, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(true, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.longHdrPackets).To(HaveLen(1))
@@ -237,7 +237,7 @@ var _ = Describe("Packet packer", func() {
 				ack := &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 10}}}
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, true)
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true).Return(ack)
-				p, err := packer.PackCoalescedPacket(true, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(true, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.longHdrPackets).To(BeEmpty())
@@ -253,7 +253,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(getSealer(), nil)
 				ack := &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 10}}}
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true).Return(ack)
-				p, buffer, err := packer.PackAckOnlyPacket(maxPacketSize, protocol.Version1)
+				p, buffer, err := packer.PackAckOnlyPacket(maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.Ack).To(Equal(ack))
@@ -284,7 +284,7 @@ var _ = Describe("Packet packer", func() {
 				framer.EXPECT().AppendStreamFrames(gomock.Any(), gomock.Any(), protocol.Version1).DoAndReturn(func(frames []ackhandler.StreamFrame, _ protocol.ByteCount, _ protocol.Version) ([]ackhandler.StreamFrame, protocol.ByteCount) {
 					return frames, 0
 				})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(p).ToNot(BeNil())
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
@@ -296,7 +296,7 @@ var _ = Describe("Packet packer", func() {
 			})
 
 			It("doesn't add an ACK-only 0-RTT packet", func() { // ACK frames cannot be sent in 0-RTT packets
-				p, err := packer.PackCoalescedPacket(true, protocol.MaxByteCount, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(true, protocol.MaxByteCount, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).To(BeNil())
 			})
@@ -471,7 +471,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(getSealer(), nil)
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true)
 				framer.EXPECT().HasData()
-				_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+				_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).To(MatchError(errNothingToPack))
 			})
 
@@ -489,7 +489,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: f})
 				buffer := getPacketBuffer()
 				buffer.Data = append(buffer.Data, []byte("foobar")...)
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				b, err := f.Append(nil, protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
@@ -507,7 +507,7 @@ var _ = Describe("Packet packer", func() {
 				framer.EXPECT().HasData()
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true).Return(ack)
 				sealingManager.EXPECT().Get1RTTSealer().Return(getSealer(), nil)
-				p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(p.Ack).To(Equal(ack))
 			})
@@ -525,7 +525,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendControlFrames(frames...)
 				expectAppendStreamFrames()
 				buffer := getPacketBuffer()
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.Frames).To(HaveLen(2))
 				var sawResetStream, sawMaxData bool
@@ -559,7 +559,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendControlFrames(frames...)
 				expectAppendStreamFrames()
 				buffer := getPacketBuffer()
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.Frames).To(HaveLen(3))
 				for i, f := range p.Frames {
@@ -595,7 +595,7 @@ var _ = Describe("Packet packer", func() {
 
 				framer.EXPECT().HasData()
 				buffer := getPacketBuffer()
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.Frames).To(HaveLen(1))
 				Expect(p.Frames[0].Frame).To(Equal(f))
@@ -623,7 +623,7 @@ var _ = Describe("Packet packer", func() {
 
 				framer.EXPECT().HasData()
 				buffer := getPacketBuffer()
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.Ack).ToNot(BeNil())
 				Expect(p.Frames).To(BeEmpty())
@@ -652,7 +652,7 @@ var _ = Describe("Packet packer", func() {
 
 				framer.EXPECT().HasData()
 				buffer := getPacketBuffer()
-				p, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).To(MatchError(errNothingToPack))
 				Expect(p.Frames).To(BeEmpty())
 				Expect(p.Ack).To(BeNil())
@@ -666,8 +666,9 @@ var _ = Describe("Packet packer", func() {
 				framer.EXPECT().HasData().Return(true)
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false)
 				var maxSize protocol.ByteCount
+				now := time.Now()
 				gomock.InOrder(
-					framer.EXPECT().AppendControlFrames(gomock.Any(), gomock.Any(), protocol.Version1).DoAndReturn(func(fs []ackhandler.Frame, maxLen protocol.ByteCount, _ protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
+					framer.EXPECT().AppendControlFrames(gomock.Any(), gomock.Any(), now, protocol.Version1).DoAndReturn(func(fs []ackhandler.Frame, maxLen protocol.ByteCount, _ time.Time, _ protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
 						maxSize = maxLen
 						return fs, 444
 					}),
@@ -676,7 +677,7 @@ var _ = Describe("Packet packer", func() {
 						return fs, 0
 					}),
 				)
-				_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+				_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, now, protocol.Version1)
 				Expect(err).To(MatchError(errNothingToPack))
 			})
 
@@ -689,7 +690,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				packer.retransmissionQueue.addHandshake(&wire.PingFrame{})
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, false)
-				packet, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				packet, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(packet).ToNot(BeNil())
 				Expect(packet.longHdrPackets).To(HaveLen(1))
@@ -731,7 +732,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendControlFrames()
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: f})
 				buffer := getPacketBuffer()
-				_, err := packer.AppendPacket(buffer, maxPacketSize, protocol.Version1)
+				_, err := packer.AppendPacket(buffer, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				// cut off the tag that the mock sealer added
 				buffer.Data = buffer.Data[:buffer.Len()-protocol.ByteCount(sealer.Overhead())]
@@ -780,7 +781,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false)
 				expectAppendControlFrames()
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: f1}, ackhandler.StreamFrame{Frame: f2}, ackhandler.StreamFrame{Frame: f3})
-				p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+				p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.Frames).To(BeEmpty())
 				Expect(p.StreamFrames).To(HaveLen(3))
@@ -799,7 +800,7 @@ var _ = Describe("Packet packer", func() {
 						ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false).Return(&wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 1}}})
 						expectAppendControlFrames()
 						expectAppendStreamFrames()
-						p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+						p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 						Expect(err).ToNot(HaveOccurred())
 						Expect(p.Ack).ToNot(BeNil())
 						Expect(p.Frames).To(BeEmpty())
@@ -815,7 +816,7 @@ var _ = Describe("Packet packer", func() {
 					ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false).Return(&wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 1}}})
 					expectAppendControlFrames()
 					expectAppendStreamFrames()
-					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					var hasPing bool
 					for _, f := range p.Frames {
@@ -833,7 +834,7 @@ var _ = Describe("Packet packer", func() {
 					ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false).Return(&wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 1}}})
 					expectAppendControlFrames()
 					expectAppendStreamFrames()
-					p, err = packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+					p, err = packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p.Ack).ToNot(BeNil())
 					Expect(p.Frames).To(BeEmpty())
@@ -848,7 +849,7 @@ var _ = Describe("Packet packer", func() {
 					expectAppendControlFrames()
 					expectAppendStreamFrames()
 					ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false)
-					_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+					_, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).To(MatchError(errNothingToPack))
 					// now add some frame to send
 					expectAppendControlFrames()
@@ -859,7 +860,7 @@ var _ = Describe("Packet packer", func() {
 					framer.EXPECT().HasData().Return(true)
 					ack := &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 1}}}
 					ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false).Return(ack)
-					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p.Ack).To(Equal(ack))
 					var hasPing bool
@@ -881,7 +882,7 @@ var _ = Describe("Packet packer", func() {
 					ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, false)
 					expectAppendStreamFrames()
 					expectAppendControlFrames(ackhandler.Frame{Frame: &wire.MaxDataFrame{}})
-					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, protocol.Version1)
+					p, err := packer.AppendPacket(getPacketBuffer(), maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p.Frames).ToNot(ContainElement(&wire.PingFrame{}))
 				})
@@ -897,7 +898,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().GetInitialSealer().Return(nil, handshake.ErrKeysDropped)
 				sealingManager.EXPECT().GetHandshakeSealer().Return(getSealer(), nil)
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.IsOnlyShortHeaderPacket()).To(BeFalse())
@@ -914,7 +915,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, false)
 				initialStream.Write([]byte("initial"))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeNumerically(">=", protocol.MinInitialPacketSize))
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
@@ -936,7 +937,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, true)
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, false)
 				handshakeStream.Write(bytes.Repeat([]byte{'f'}, 2000))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.longHdrPackets[0].frames).To(HaveLen(1))
@@ -956,7 +957,7 @@ var _ = Describe("Packet packer", func() {
 				// don't EXPECT any calls for a Handshake ACK frame
 				initialStream.Write([]byte("initial"))
 				handshakeStream.Write([]byte("handshake"))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
 				Expect(p.longHdrPackets).To(HaveLen(2))
@@ -986,7 +987,7 @@ var _ = Describe("Packet packer", func() {
 				// don't EXPECT any calls for a Handshake ACK frame
 				initialStream.Write([]byte("initial"))
 				packer.retransmissionQueue.addHandshake(&wire.PingFrame{})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
 				Expect(p.longHdrPackets).To(HaveLen(2))
@@ -1015,7 +1016,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, gomock.Any())
 				packer.retransmissionQueue.addInitial(&wire.PingFrame{})
 				packer.retransmissionQueue.addHandshake(&wire.PingFrame{})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
 				Expect(p.longHdrPackets).To(HaveLen(2))
@@ -1046,7 +1047,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendStreamFrames()
 				framer.EXPECT().HasData().Return(true)
 				packer.retransmissionQueue.addAppData(&wire.PingFrame{})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.IsOnlyShortHeaderPacket()).To(BeFalse())
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
@@ -1080,7 +1081,7 @@ var _ = Describe("Packet packer", func() {
 				initialStream.Write([]byte("initial"))
 				expectAppendControlFrames()
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: &wire.StreamFrame{Data: []byte("foobar")}})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeNumerically(">=", protocol.MinInitialPacketSize))
 				Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
@@ -1114,7 +1115,7 @@ var _ = Describe("Packet packer", func() {
 				handshakeStream.Write([]byte("handshake"))
 				expectAppendControlFrames()
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: &wire.StreamFrame{Data: []byte("foobar")}})
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.buffer.Len()).To(BeNumerically("<", 100))
 				Expect(p.longHdrPackets).To(HaveLen(1))
@@ -1140,7 +1141,7 @@ var _ = Describe("Packet packer", func() {
 				// don't EXPECT any calls to GetHandshakeSealer and Get1RTTSealer
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, false)
 				handshakeStream.Write(bytes.Repeat([]byte{'f'}, int(maxPacketSize-protocol.MinCoalescedPacketSize-(&wire.CryptoFrame{}).Length(protocol.Version1))))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.shortHdrPacket).To(BeNil())
@@ -1161,7 +1162,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				packer.retransmissionQueue.addHandshake(&wire.PingFrame{})
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionHandshake, false)
-				packet, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				packet, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(packet).ToNot(BeNil())
 				Expect(packet.longHdrPackets).To(HaveLen(1))
@@ -1198,7 +1199,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().GetHandshakeSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, false)
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.longHdrPackets[0].EncryptionLevel()).To(Equal(protocol.EncryptionInitial))
@@ -1215,7 +1216,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
 				pnManager.EXPECT().PopPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.longHdrPackets[0].ack).To(Equal(ack))
@@ -1226,7 +1227,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().GetHandshakeSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, true)
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).To(BeNil())
 			})
@@ -1240,7 +1241,7 @@ var _ = Describe("Packet packer", func() {
 				sealingManager.EXPECT().Get1RTTSealer().Return(nil, handshake.ErrKeysNotYetAvailable)
 				pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
 				pnManager.EXPECT().PopPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42))
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.longHdrPackets[0].ack).To(Equal(ack))
@@ -1261,7 +1262,7 @@ var _ = Describe("Packet packer", func() {
 					ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, false)
 					initialStream.Write([]byte("foobar"))
 					packer.perspective = protocol.PerspectiveClient
-					p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+					p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p.buffer.Len()).To(BeNumerically(">=", protocol.MinInitialPacketSize))
 					Expect(p.buffer.Len()).To(BeEquivalentTo(maxPacketSize))
@@ -1284,7 +1285,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.EncryptionInitial, false).Return(ack)
 				initialStream.Write([]byte("foobar"))
 				packer.perspective = protocol.PerspectiveClient
-				p, err := packer.PackCoalescedPacket(false, maxPacketSize, protocol.Version1)
+				p, err := packer.PackCoalescedPacket(false, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p.longHdrPackets).To(HaveLen(1))
 				Expect(p.longHdrPackets[0].ack).To(Equal(ack))
@@ -1306,7 +1307,7 @@ var _ = Describe("Packet packer", func() {
 					pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
 					pnManager.EXPECT().PopPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42))
 
-					p, err := packer.MaybePackProbePacket(protocol.EncryptionInitial, maxPacketSize, protocol.Version1)
+					p, err := packer.MaybePackProbePacket(protocol.EncryptionInitial, maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p).ToNot(BeNil())
 					Expect(p.longHdrPackets).To(HaveLen(1))
@@ -1327,7 +1328,7 @@ var _ = Describe("Packet packer", func() {
 					pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen1)
 					pnManager.EXPECT().PopPacketNumber(protocol.EncryptionInitial).Return(protocol.PacketNumber(0x42))
 
-					p, err := packer.MaybePackProbePacket(protocol.EncryptionInitial, maxPacketSize, protocol.Version1)
+					p, err := packer.MaybePackProbePacket(protocol.EncryptionInitial, maxPacketSize, time.Now(), protocol.Version1)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(err).ToNot(HaveOccurred())
 					Expect(p).ToNot(BeNil())
@@ -1350,7 +1351,7 @@ var _ = Describe("Packet packer", func() {
 				pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
 				pnManager.EXPECT().PopPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42))
 
-				p, err := packer.MaybePackProbePacket(protocol.EncryptionHandshake, maxPacketSize, protocol.Version1)
+				p, err := packer.MaybePackProbePacket(protocol.EncryptionHandshake, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).ToNot(BeNil())
@@ -1370,7 +1371,7 @@ var _ = Describe("Packet packer", func() {
 				pnManager.EXPECT().PeekPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42), protocol.PacketNumberLen2)
 				pnManager.EXPECT().PopPacketNumber(protocol.EncryptionHandshake).Return(protocol.PacketNumber(0x42))
 
-				p, err := packer.MaybePackProbePacket(protocol.EncryptionHandshake, maxPacketSize, protocol.Version1)
+				p, err := packer.MaybePackProbePacket(protocol.EncryptionHandshake, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).ToNot(BeNil())
@@ -1394,7 +1395,7 @@ var _ = Describe("Packet packer", func() {
 				expectAppendControlFrames()
 				expectAppendStreamFrames(ackhandler.StreamFrame{Frame: f})
 
-				p, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, protocol.Version1)
+				p, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.IsOnlyShortHeaderPacket()).To(BeTrue())
@@ -1421,7 +1422,7 @@ var _ = Describe("Packet packer", func() {
 					return append(fs, ackhandler.StreamFrame{Frame: sf}), sf.Length(v)
 				})
 
-				p, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, protocol.Version1)
+				p, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(p).ToNot(BeNil())
 				Expect(p.IsOnlyShortHeaderPacket()).To(BeTrue())
@@ -1439,7 +1440,7 @@ var _ = Describe("Packet packer", func() {
 				ackFramer.EXPECT().GetAckFrame(protocol.Encryption1RTT, true)
 				framer.EXPECT().HasData()
 
-				packet, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, protocol.Version1)
+				packet, err := packer.MaybePackProbePacket(protocol.Encryption1RTT, maxPacketSize, time.Now(), protocol.Version1)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(packet).To(BeNil())
 			})
diff --git a/qlog/json_helper_test.go b/qlog/json_helper_test.go
index d63cb41c..77b94f84 100644
--- a/qlog/json_helper_test.go
+++ b/qlog/json_helper_test.go
@@ -11,7 +11,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-//nolint:unparam
 func scaleDuration(t time.Duration) time.Duration {
 	scaleFactor := 1
 	if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
diff --git a/receive_stream_test.go b/receive_stream_test.go
index cb4eaa1a..95a4c847 100644
--- a/receive_stream_test.go
+++ b/receive_stream_test.go
@@ -43,13 +43,14 @@ var _ = Describe("Receive Stream", func() {
 
 	Context("reading", func() {
 		It("reads a single STREAM frame", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			now := time.Now()
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, now)
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(4))
 			frame := wire.StreamFrame{
 				Offset: 0,
 				Data:   []byte{0xDE, 0xAD, 0xBE, 0xEF},
 			}
-			Expect(str.handleStreamFrame(&frame)).To(Succeed())
+			Expect(str.handleStreamFrame(&frame, now)).To(Succeed())
 			b := make([]byte, 4)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -58,14 +59,14 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("reads a single STREAM frame in multiple goes", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2))
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2))
 			frame := wire.StreamFrame{
 				Offset: 0,
 				Data:   []byte{0xDE, 0xAD, 0xBE, 0xEF},
 			}
-			Expect(str.handleStreamFrame(&frame)).To(Succeed())
+			Expect(str.handleStreamFrame(&frame, time.Now())).To(Succeed())
 			b := make([]byte, 2)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -78,27 +79,29 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("queues a flow control update", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			now := time.Now()
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, now)
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(3)).Return(true)
 			frame := wire.StreamFrame{
 				Offset: 0,
 				Data:   []byte{0xde, 0xad, 0xbe, 0xef},
 			}
-			Expect(str.handleStreamFrame(&frame)).To(Succeed())
+			Expect(str.handleStreamFrame(&frame, now)).To(Succeed())
 			mockSender.EXPECT().onHasStreamControlFrame(streamID, str)
 			n, err := strWithTimeout.Read(make([]byte, 3))
 			Expect(err).ToNot(HaveOccurred())
 			Expect(n).To(Equal(3))
-			mockFC.EXPECT().GetWindowUpdate().Return(protocol.ByteCount(1337))
-			f, ok, hasMore := str.getControlFrame()
+			now = now.Add(time.Second)
+			mockFC.EXPECT().GetWindowUpdate(now).Return(protocol.ByteCount(1337))
+			f, ok, hasMore := str.getControlFrame(now)
 			Expect(ok).To(BeTrue())
 			Expect(f.Frame).To(Equal(&wire.MaxStreamDataFrame{StreamID: streamID, MaximumStreamData: 1337}))
 			Expect(hasMore).To(BeFalse())
 		})
 
 		It("reads all data available", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2)).Times(2)
 			frame1 := wire.StreamFrame{
 				Offset: 0,
@@ -108,10 +111,8 @@ var _ = Describe("Receive Stream", func() {
 				Offset: 2,
 				Data:   []byte{0xBE, 0xEF},
 			}
-			err := str.handleStreamFrame(&frame1)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame2)
-			Expect(err).ToNot(HaveOccurred())
+			Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
 			b := make([]byte, 6)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -120,8 +121,8 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("assembles multiple STREAM frames", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2)).Times(2)
 			frame1 := wire.StreamFrame{
 				Offset: 0,
@@ -131,10 +132,8 @@ var _ = Describe("Receive Stream", func() {
 				Offset: 2,
 				Data:   []byte{0xBE, 0xEF},
 			}
-			err := str.handleStreamFrame(&frame1)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame2)
-			Expect(err).ToNot(HaveOccurred())
+			Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
 			b := make([]byte, 4)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -143,14 +142,13 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("waits until data is available", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2))
 			go func() {
 				defer GinkgoRecover()
 				frame := wire.StreamFrame{Data: []byte{0xDE, 0xAD}}
 				time.Sleep(10 * time.Millisecond)
-				err := str.handleStreamFrame(&frame)
-				Expect(err).ToNot(HaveOccurred())
+				Expect(str.handleStreamFrame(&frame, time.Now())).To(Succeed())
 			}()
 			b := make([]byte, 2)
 			n, err := strWithTimeout.Read(b)
@@ -159,8 +157,8 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("handles STREAM frames in wrong order", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2)).Times(2)
 			frame1 := wire.StreamFrame{
 				Offset: 2,
@@ -170,10 +168,8 @@ var _ = Describe("Receive Stream", func() {
 				Offset: 0,
 				Data:   []byte{0xDE, 0xAD},
 			}
-			err := str.handleStreamFrame(&frame1)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame2)
-			Expect(err).ToNot(HaveOccurred())
+			Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
 			b := make([]byte, 4)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -182,9 +178,9 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("ignores duplicate STREAM frames", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2)).Times(2)
 			frame1 := wire.StreamFrame{
 				Offset: 0,
@@ -198,12 +194,9 @@ var _ = Describe("Receive Stream", func() {
 				Offset: 2,
 				Data:   []byte{0xBE, 0xEF},
 			}
-			err := str.handleStreamFrame(&frame1)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame2)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame3)
-			Expect(err).ToNot(HaveOccurred())
+			Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame3, time.Now())).To(Succeed())
 			b := make([]byte, 4)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -212,8 +205,8 @@ var _ = Describe("Receive Stream", func() {
 		})
 
 		It("doesn't rejects a STREAM frames with an overlapping data range", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false)
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false)
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), false, gomock.Any())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false, gomock.Any())
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2))
 			mockFC.EXPECT().AddBytesRead(protocol.ByteCount(4))
 			frame1 := wire.StreamFrame{
@@ -224,10 +217,8 @@ var _ = Describe("Receive Stream", func() {
 				Offset: 2,
 				Data:   []byte("obar"),
 			}
-			err := str.handleStreamFrame(&frame1)
-			Expect(err).ToNot(HaveOccurred())
-			err = str.handleStreamFrame(&frame2)
-			Expect(err).ToNot(HaveOccurred())
+			Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+			Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
 			b := make([]byte, 6)
 			n, err := strWithTimeout.Read(b)
 			Expect(err).ToNot(HaveOccurred())
@@ -242,8 +233,8 @@ var _ = Describe("Receive Stream", func() {
 			})
 
 			It("returns an error when Read is called after the deadline", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false).AnyTimes()
-				Expect(str.handleStreamFrame(&wire.StreamFrame{Data: []byte("foobar")})).To(Succeed())
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false, gomock.Any()).AnyTimes()
+				Expect(str.handleStreamFrame(&wire.StreamFrame{Data: []byte("foobar")}, time.Now())).To(Succeed())
 				str.SetReadDeadline(time.Now().Add(-time.Second))
 				b := make([]byte, 6)
 				n, err := strWithTimeout.Read(b)
@@ -343,13 +334,13 @@ var _ = Describe("Receive Stream", func() {
 		Context("closing", func() {
 			Context("with FIN bit", func() {
 				It("returns EOFs", func() {
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), true)
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), true, gomock.Any())
 					mockFC.EXPECT().AddBytesRead(protocol.ByteCount(4))
-					str.handleStreamFrame(&wire.StreamFrame{
+					Expect(str.handleStreamFrame(&wire.StreamFrame{
 						Offset: 0,
 						Data:   []byte{0xDE, 0xAD, 0xBE, 0xEF},
 						Fin:    true,
-					})
+					}, time.Now())).To(Succeed())
 					mockSender.EXPECT().onStreamCompleted(streamID)
 					b := make([]byte, 4)
 					n, err := strWithTimeout.Read(b)
@@ -362,8 +353,8 @@ var _ = Describe("Receive Stream", func() {
 				})
 
 				It("handles out-of-order frames", func() {
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false)
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), true)
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), false, gomock.Any())
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(4), true, gomock.Any())
 					mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2)).Times(2)
 					frame1 := wire.StreamFrame{
 						Offset: 2,
@@ -374,10 +365,8 @@ var _ = Describe("Receive Stream", func() {
 						Offset: 0,
 						Data:   []byte{0xDE, 0xAD},
 					}
-					err := str.handleStreamFrame(&frame1)
-					Expect(err).ToNot(HaveOccurred())
-					err = str.handleStreamFrame(&frame2)
-					Expect(err).ToNot(HaveOccurred())
+					Expect(str.handleStreamFrame(&frame1, time.Now())).To(Succeed())
+					Expect(str.handleStreamFrame(&frame2, time.Now())).To(Succeed())
 					mockSender.EXPECT().onStreamCompleted(streamID)
 					b := make([]byte, 4)
 					n, err := strWithTimeout.Read(b)
@@ -390,14 +379,13 @@ var _ = Describe("Receive Stream", func() {
 				})
 
 				It("returns EOFs with partial read", func() {
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), true)
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(2), true, gomock.Any())
 					mockFC.EXPECT().AddBytesRead(protocol.ByteCount(2))
-					err := str.handleStreamFrame(&wire.StreamFrame{
+					Expect(str.handleStreamFrame(&wire.StreamFrame{
 						Offset: 0,
 						Data:   []byte{0xde, 0xad},
 						Fin:    true,
-					})
-					Expect(err).ToNot(HaveOccurred())
+					}, time.Now())).To(Succeed())
 					mockSender.EXPECT().onStreamCompleted(streamID)
 					b := make([]byte, 4)
 					n, err := strWithTimeout.Read(b)
@@ -407,13 +395,12 @@ var _ = Describe("Receive Stream", func() {
 				})
 
 				It("handles immediate FINs", func() {
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(0), true)
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(0), true, gomock.Any())
 					mockFC.EXPECT().AddBytesRead(protocol.ByteCount(0))
-					err := str.handleStreamFrame(&wire.StreamFrame{
+					Expect(str.handleStreamFrame(&wire.StreamFrame{
 						Offset: 0,
 						Fin:    true,
-					})
-					Expect(err).ToNot(HaveOccurred())
+					}, time.Now())).To(Succeed())
 					mockSender.EXPECT().onStreamCompleted(streamID)
 					b := make([]byte, 4)
 					n, err := strWithTimeout.Read(b)
@@ -429,7 +416,7 @@ var _ = Describe("Receive Stream", func() {
 				// Note that even without the protection built into the receiveStream, this test
 				// is very timing-dependent, and would need to run a few hundred times to trigger the failure.
 				It("handles concurrent reads", func() {
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), gomock.Any()).AnyTimes()
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), gomock.Any(), gomock.Any()).AnyTimes()
 					var bytesRead protocol.ByteCount
 					mockFC.EXPECT().AddBytesRead(gomock.Any()).Do(func(n protocol.ByteCount) bool {
 						bytesRead += n
@@ -451,11 +438,11 @@ var _ = Describe("Receive Stream", func() {
 							Expect(err).To(MatchError(io.EOF))
 						}()
 					}
-					str.handleStreamFrame(&wire.StreamFrame{
+					Expect(str.handleStreamFrame(&wire.StreamFrame{
 						Offset: 0,
 						Data:   []byte("foobar"),
 						Fin:    true,
-					})
+					}, time.Now())).To(Succeed())
 					wg.Wait()
 					Expect(bytesRead).To(BeEquivalentTo(6))
 					Expect(atomic.LoadInt32(&numCompleted)).To(BeEquivalentTo(1))
@@ -537,7 +524,7 @@ var _ = Describe("Receive Stream", func() {
 			It("queues a STOP_SENDING frame", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(gomock.Any(), gomock.Any())
 				str.CancelRead(1234)
-				f, ok, hasMore := str.getControlFrame()
+				f, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(f.Frame).To(Equal(&wire.StopSendingFrame{
 					StreamID:  streamID,
@@ -547,14 +534,14 @@ var _ = Describe("Receive Stream", func() {
 			})
 
 			It("doesn't send a STOP_SENDING frame, if the FIN was already read", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), true)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), true, gomock.Any())
 				mockFC.EXPECT().AddBytesRead(protocol.ByteCount(6))
 				// no calls to mockSender.queueControlFrame
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					StreamID: streamID,
 					Data:     []byte("foobar"),
 					Fin:      true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 				mockSender.EXPECT().onStreamCompleted(streamID)
 				n, err := strWithTimeout.Read(make([]byte, 100))
 				Expect(err).To(MatchError(io.EOF))
@@ -563,13 +550,13 @@ var _ = Describe("Receive Stream", func() {
 			})
 
 			It("doesn't send a STOP_SENDING frame, if the stream was already reset", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any())
 				mockFC.EXPECT().Abandon().MinTimes(1)
 				Expect(str.handleResetStreamFrame(&wire.ResetStreamFrame{
 					ErrorCode: 1337,
 					StreamID:  streamID,
 					FinalSize: 42,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 				mockSender.EXPECT().onStreamCompleted(gomock.Any())
 				str.CancelRead(1234)
 				// check that the error indicates a remote reset
@@ -583,11 +570,11 @@ var _ = Describe("Receive Stream", func() {
 			})
 
 			It("sends a STOP_SENDING after receiving the final offset", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), true)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), true, gomock.Any())
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					Data: []byte("foobar"),
 					Fin:  true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 				mockFC.EXPECT().Abandon()
 				mockSender.EXPECT().onHasStreamControlFrame(gomock.Any(), gomock.Any())
 				mockSender.EXPECT().onStreamCompleted(streamID)
@@ -602,33 +589,33 @@ var _ = Describe("Receive Stream", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(gomock.Any(), gomock.Any())
 				str.CancelRead(1234)
 				gomock.InOrder(
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true),
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true, gomock.Any()),
 					mockFC.EXPECT().Abandon(),
 				)
 				mockSender.EXPECT().onStreamCompleted(streamID)
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					Offset: 1000,
 					Fin:    true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 			})
 
 			It("handles duplicate FinBits after the stream was canceled", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(gomock.Any(), gomock.Any())
 				str.CancelRead(1234)
 				gomock.InOrder(
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true),
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true, gomock.Any()),
 					mockFC.EXPECT().Abandon(),
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true),
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1000), true, gomock.Any()),
 				)
 				mockSender.EXPECT().onStreamCompleted(streamID)
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					Offset: 1000,
 					Fin:    true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					Offset: 1000,
 					Fin:    true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 			})
 
 			It("ignores cancellations after closeForShutdown", func() {
@@ -665,20 +652,20 @@ var _ = Describe("Receive Stream", func() {
 				Consistently(done).ShouldNot(BeClosed())
 				mockSender.EXPECT().onStreamCompleted(streamID)
 				gomock.InOrder(
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true),
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any()),
 					mockFC.EXPECT().Abandon(),
 				)
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 				Eventually(done).Should(BeClosed())
 			})
 
 			It("doesn't allow further calls to Read", func() {
 				mockSender.EXPECT().onStreamCompleted(streamID)
 				gomock.InOrder(
-					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true),
+					mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any()),
 					mockFC.EXPECT().Abandon(),
 				)
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 				_, err := strWithTimeout.Read([]byte{0})
 				Expect(err).To(MatchError(&StreamError{
 					StreamID:  streamID,
@@ -688,28 +675,27 @@ var _ = Describe("Receive Stream", func() {
 
 			It("errors when receiving a RESET_STREAM with an inconsistent offset", func() {
 				testErr := errors.New("already received a different final offset before")
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true).Return(testErr)
-				err := str.handleResetStreamFrame(rst)
-				Expect(err).To(MatchError(testErr))
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any()).Return(testErr)
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(MatchError(testErr))
 			})
 
 			It("ignores duplicate RESET_STREAM frames", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true).Times(2)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any()).Times(2)
 				mockFC.EXPECT().Abandon()
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 			})
 
 			It("doesn't call onStreamCompleted again when the final offset was already received via Fin", func() {
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true).Times(2)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any()).Times(2)
 				Expect(str.handleStreamFrame(&wire.StreamFrame{
 					StreamID: streamID,
 					Offset:   rst.FinalSize,
 					Fin:      true,
-				})).To(Succeed())
+				}, time.Now())).To(Succeed())
 				mockFC.EXPECT().Abandon().MinTimes(1)
 				mockSender.EXPECT().onStreamCompleted(streamID)
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 				// now read the error
 				n, err := str.Read([]byte{0})
 				Expect(err).To(HaveOccurred())
@@ -718,17 +704,16 @@ var _ = Describe("Receive Stream", func() {
 
 			It("doesn't do anything when it was closed for shutdown", func() {
 				str.closeForShutdown(errors.New("shutdown"))
-				err := str.handleResetStreamFrame(rst)
-				Expect(err).ToNot(HaveOccurred())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 			})
 
 			It("handles RESET_STREAM after CancelRead", func() {
 				mockFC.EXPECT().Abandon()
 				mockSender.EXPECT().onHasStreamControlFrame(gomock.Any(), gomock.Any())
 				str.CancelRead(1234)
-				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true)
+				mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(42), true, gomock.Any())
 				mockSender.EXPECT().onStreamCompleted(streamID)
-				Expect(str.handleResetStreamFrame(rst)).To(Succeed())
+				Expect(str.handleResetStreamFrame(rst, time.Now())).To(Succeed())
 				// check that the error indicates a local reset
 				n, err := str.Read([]byte{0})
 				Expect(err).To(HaveOccurred())
@@ -742,12 +727,10 @@ var _ = Describe("Receive Stream", func() {
 
 	It("errors when a STREAM frame causes a flow control violation", func() {
 		testErr := errors.New("flow control violation")
-		mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(8), false).Return(testErr)
-		frame := wire.StreamFrame{
+		mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(8), false, gomock.Any()).Return(testErr)
+		Expect(str.handleStreamFrame(&wire.StreamFrame{
 			Offset: 2,
 			Data:   []byte("foobar"),
-		}
-		err := str.handleStreamFrame(&frame)
-		Expect(err).To(MatchError(testErr))
+		}, time.Now())).To(MatchError(testErr))
 	})
 })
diff --git a/send_stream_test.go b/send_stream_test.go
index c5d62436..81717502 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -345,7 +345,7 @@ var _ = Describe("Send Stream", func() {
 				_, ok, hasMoreData = str.popStreamFrame(1000, protocol.Version1)
 				Expect(ok).To(BeFalse())
 				Expect(hasMoreData).To(BeFalse())
-				cf, ok, hasMore := str.getControlFrame()
+				cf, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(cf.Frame).To(Equal(&wire.StreamDataBlockedFrame{
 					StreamID:          streamID,
@@ -692,7 +692,7 @@ var _ = Describe("Send Stream", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(streamID, gomock.Any())
 				str.writeOffset = 1234
 				str.CancelWrite(9876)
-				cf, ok, hasMore := str.getControlFrame()
+				cf, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(cf.Frame).To(Equal(&wire.ResetStreamFrame{
 					StreamID:  streamID,
@@ -705,13 +705,13 @@ var _ = Describe("Send Stream", func() {
 			It("retransmits a RESET_STREAM frame", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(streamID, gomock.Any())
 				str.CancelWrite(9876)
-				cf, ok, _ := str.getControlFrame()
+				cf, ok, _ := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(cf.Frame).To(BeAssignableToTypeOf(&wire.ResetStreamFrame{}))
 
 				mockSender.EXPECT().onHasStreamControlFrame(streamID, gomock.Any())
 				cf.Handler.OnLost(cf.Frame)
-				cf2, ok, _ := str.getControlFrame()
+				cf2, ok, _ := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(cf2.Frame).To(Equal(cf.Frame))
 			})
@@ -864,11 +864,11 @@ var _ = Describe("Send Stream", func() {
 				mockSender.EXPECT().onHasStreamControlFrame(streamID, gomock.Any())
 				str.CancelWrite(1234)
 				str.CancelWrite(4321)
-				cf, ok, hasMore := str.getControlFrame()
+				cf, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(cf.Frame).To(Equal(&wire.ResetStreamFrame{StreamID: streamID, ErrorCode: 1234}))
 				Expect(hasMore).To(BeFalse())
-				cf, ok, hasMore = str.getControlFrame()
+				cf, ok, hasMore = str.getControlFrame(time.Now())
 				Expect(ok).To(BeFalse())
 				Expect(cf.Frame).To(BeNil())
 				Expect(hasMore).To(BeFalse())
@@ -880,7 +880,7 @@ var _ = Describe("Send Stream", func() {
 				Expect(str.Close()).To(Succeed())
 				// don't EXPECT any calls to queueControlFrame
 				str.CancelWrite(123)
-				f, ok, hasMore := str.getControlFrame()
+				f, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(f.Frame).To(BeAssignableToTypeOf(&wire.ResetStreamFrame{}))
 				Expect(hasMore).To(BeFalse())
@@ -896,7 +896,7 @@ var _ = Describe("Send Stream", func() {
 					StreamID:  streamID,
 					ErrorCode: 101,
 				})
-				f, ok, hasMore := str.getControlFrame()
+				f, ok, hasMore := str.getControlFrame(time.Now())
 				Expect(ok).To(BeTrue())
 				Expect(f.Frame).To(Equal(&wire.ResetStreamFrame{
 					StreamID:  streamID,
@@ -1168,7 +1168,7 @@ var _ = Describe("Send Stream", func() {
 
 			mockSender.EXPECT().onHasStreamControlFrame(streamID, str)
 			str.CancelWrite(1234)
-			cf, ok, _ := str.getControlFrame()
+			cf, ok, _ := str.getControlFrame(time.Now())
 			Expect(ok).To(BeTrue())
 
 			// Acknowledge the STREAM frame.
diff --git a/stream_test.go b/stream_test.go
index 5d0e64b5..cd747540 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -15,6 +15,7 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/gbytes"
+	"go.uber.org/mock/gomock"
 )
 
 // in the tests for the stream deadlines we set a deadline
@@ -67,10 +68,8 @@ var _ = Describe("Stream", func() {
 		})
 
 		It("sets a read deadline, when SetDeadline is called", func() {
-			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false).AnyTimes()
-			f := &wire.StreamFrame{Data: []byte("foobar")}
-			err := str.handleStreamFrame(f)
-			Expect(err).ToNot(HaveOccurred())
+			mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(6), false, gomock.Any()).AnyTimes()
+			Expect(str.handleStreamFrame(&wire.StreamFrame{Data: []byte("foobar")}, time.Now())).To(Succeed())
 			str.SetDeadline(time.Now().Add(-time.Second))
 			b := make([]byte, 6)
 			n, err := strWithTimeout.Read(b)
diff --git a/sys_conn_df_darwin_test.go b/sys_conn_df_darwin_test.go
new file mode 100644
index 00000000..335003ef
--- /dev/null
+++ b/sys_conn_df_darwin_test.go
@@ -0,0 +1,99 @@
+package quic
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIPFragmentation(t *testing.T) {
+	sink, err := net.ListenUDP("udp", &net.UDPAddr{Port: 0})
+	require.NoError(t, err)
+	t.Cleanup(func() { sink.Close() })
+	sinkPort := sink.LocalAddr().(*net.UDPAddr).Port
+
+	canSendIPv4 := func(conn *net.UDPConn) bool {
+		_, err := conn.WriteTo([]byte("hello"), &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: sinkPort})
+		return err == nil
+	}
+
+	canSendIPv6 := func(conn *net.UDPConn) bool {
+		_, err := conn.WriteTo([]byte("hello"), &net.UDPAddr{IP: net.IPv6loopback, Port: sinkPort})
+		return err == nil
+	}
+
+	t.Run("udp4", func(t *testing.T) {
+		conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+		require.NoError(t, err)
+		defer conn.Close()
+
+		require.True(t, canSendIPv4(conn))
+		require.False(t, canSendIPv6(conn))
+
+		raw, err := conn.SyscallConn()
+		require.NoError(t, err)
+		canDF, _ := setDF(raw)
+		require.True(t, canDF)
+	})
+
+	t.Run("udp6", func(t *testing.T) {
+		conn, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: 0})
+		require.NoError(t, err)
+		defer conn.Close()
+
+		require.False(t, canSendIPv4(conn))
+		require.True(t, canSendIPv6(conn))
+
+		raw, err := conn.SyscallConn()
+		require.NoError(t, err)
+		canDF, _ := setDF(raw)
+		require.True(t, canDF)
+	})
+
+	t.Run("udp, dual-stack", func(t *testing.T) {
+		if version, err := getMacOSVersion(); err != nil || version < macOSVersion15 {
+			t.Skipf("skipping on darwin %d", version-9)
+		}
+
+		conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 0})
+		require.NoError(t, err)
+		defer conn.Close()
+
+		require.True(t, canSendIPv4(conn))
+		require.True(t, canSendIPv6(conn))
+
+		raw, err := conn.SyscallConn()
+		require.NoError(t, err)
+		canDF, _ := setDF(raw)
+		require.True(t, canDF)
+	})
+
+	t.Run("udp, listening on IPv4", func(t *testing.T) {
+		conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
+		require.NoError(t, err)
+		defer conn.Close()
+
+		require.True(t, canSendIPv4(conn))
+		require.False(t, canSendIPv6(conn))
+
+		raw, err := conn.SyscallConn()
+		require.NoError(t, err)
+		canDF, _ := setDF(raw)
+		require.True(t, canDF)
+	})
+
+	t.Run("udp, listening on IPv6", func(t *testing.T) {
+		conn, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6loopback, Port: 0})
+		require.NoError(t, err)
+		defer conn.Close()
+
+		require.False(t, canSendIPv4(conn))
+		require.True(t, canSendIPv6(conn))
+
+		raw, err := conn.SyscallConn()
+		require.NoError(t, err)
+		canDF, _ := setDF(raw)
+		require.True(t, canDF)
+	})
+}