From 58100f2720c9f16dd6c91c99c3fe7e9f51abe461 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 10:37:05 -1000 Subject: [PATCH] [api] Fix batch messages stuck in Nagle buffer and missing flush on list entities completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-message batch path in process_batch_multi_ bypassed set_nodelay_for_message(), so batch data written to the socket would sit in LWIP's Nagle buffer when log messages had previously enabled Nagle. The remote wouldn't ACK until it sent its own data (e.g. a ping), which could take 20+ seconds. This caused log-only API clients (like esphome logs) to time out waiting for ListEntitiesDoneResponse. Additionally, when LIST_ENTITIES completed without a state subscription, the batched entity listing responses were never explicitly flushed — they waited for the 100ms batch timer instead of being sent immediately like the INITIAL_STATE completion path already did. Fixes both issues: - Call set_nodelay_for_message(false) before write_protobuf_messages in process_batch_multi_ to ensure TCP_NODELAY is on - Extract finalize_iterator_sync_() helper and call it when LIST_ENTITIES completes without state_subscription --- 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 bfb3ec291c..969dea8629 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_(); @@ -2222,6 +2230,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 2f685b0b8a..8b7f7389c6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -618,6 +618,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