From 480ee0eef7dc5ef8cb7f1f7a6cb14a0dad096cc2 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 22 Feb 2026 20:17:51 -0800 Subject: [PATCH] ws: add buffer reclamation to prevent memory bloat on long-lived connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BytesMut retains its allocation even after being fully drained, which means a single large WebSocket message permanently inflates memory for the lifetime of the connection. This adds a configurable `max_buffer_size` threshold (default 128 KiB) — when the read buffer is empty and its capacity exceeds this limit, it gets replaced with a fresh allocation. New API: `Codec::max_buffer_size(size)` to configure the threshold. Fixes #2075 --- actix-http/src/ws/codec.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index ad487e400..8a06ac34f 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -71,6 +71,10 @@ pub enum Item { pub struct Codec { flags: Flags, max_size: usize, + /// When the read buffer is fully drained and its capacity exceeds this + /// threshold, it is replaced with a fresh allocation to reclaim memory. + /// Defaults to 128 KiB. + max_buffer_size: usize, } bitflags! { @@ -87,6 +91,7 @@ impl Codec { pub const fn new() -> Codec { Codec { max_size: 65_536, + max_buffer_size: 128 * 1024, flags: Flags::SERVER, } } @@ -100,6 +105,20 @@ impl Codec { self } + /// Set the max buffer capacity threshold for read buffer reclamation. + /// + /// When the read buffer is fully drained (empty) and its allocated capacity + /// exceeds this threshold, it is replaced with a fresh, smaller allocation. + /// This prevents long-lived connections from permanently holding memory + /// allocated for occasional large messages. + /// + /// By default this is set to 128 KiB. + #[must_use = "This returns the a new Codec, without modifying the original."] + pub fn max_buffer_size(mut self, size: usize) -> Self { + self.max_buffer_size = size; + self + } + /// Set decoder to client mode. /// /// By default decoder works in server mode. @@ -220,6 +239,14 @@ impl Decoder for Codec { type Error = ProtocolError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // Reclaim memory: if the buffer has been fully drained and its capacity + // exceeds the threshold, replace it with a fresh allocation. This prevents + // peak memory usage from becoming permanent on long-lived connections + // (e.g., WebSockets that occasionally receive large payloads). + if src.is_empty() && src.capacity() > self.max_buffer_size { + *src = BytesMut::new(); + } + match Parser::parse(src, self.flags.contains(Flags::SERVER), self.max_size) { Ok(Some((finished, opcode, payload))) => { // continuation is not supported