From d4cce142c5352e94772777dfeaf668fbc1533cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 11:11:31 -1000 Subject: [PATCH] [api] Fix batch messages stuck in Nagle buffer (#15581) --- esphome/components/api/api_connection.cpp | 33 ++++++++++++++++------- esphome/components/api/api_connection.h | 1 + 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 17605345fd..7db423141c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -315,6 +315,8 @@ void APIConnection::process_active_iterator_() { this->destroy_active_iterator_(); if (this->flags_.state_subscription) { this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } else { + this->finalize_iterator_sync_(); } } else { this->process_iterator_batch_(this->iterator_storage_.list_entities); @@ -322,21 +324,27 @@ void APIConnection::process_active_iterator_() { } else { // INITIAL_STATE if (this->iterator_storage_.initial_state.completed()) { this->destroy_active_iterator_(); - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); - } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); + this->finalize_iterator_sync_(); } else { this->process_iterator_batch_(this->iterator_storage_.initial_state); } } } +void APIConnection::finalize_iterator_sync_() { + // Flush any remaining batched messages immediately so clients + // receive completion responses (e.g. ListEntitiesDoneResponse) + // without waiting for the batch timer. + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); +} + void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { size_t initial_size = this->deferred_batch_.size(); size_t max_batch = this->get_max_batch_size_(); @@ -2185,6 +2193,13 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items shared_buf.resize(shared_buf.size() + footer_size); } + // Ensure TCP_NODELAY is on before writing batch data. + // Log messages enable Nagle (NODELAY off) to coalesce small packets. + // Without this, batch data written to the socket sits in LWIP's Nagle + // buffer — the remote won't ACK until it sends its own data (e.g. a + // ping), which can take 20+ seconds. + this->helper_->set_nodelay_for_message(false); + // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, std::span(message_info, items_processed)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f227dbe2de..284c4475de 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -662,6 +662,7 @@ class APIConnection final : public APIServerConnectionBase { // Helper methods for iterator lifecycle management void destroy_active_iterator_(); void begin_iterator_(ActiveIterator type); + void finalize_iterator_sync_(); #ifdef USE_CAMERA std::unique_ptr image_reader_; #endif