@@ -1431,6 +1431,7 @@ async fn tunnel_loop(
14311431 let inflight_cap = INFLIGHT_ACTIVE ;
14321432 let mut max_inflight = INFLIGHT_OPTIMIST . min ( inflight_cap) ;
14331433 let mut consecutive_empty = 0u32 ;
1434+ let mut idle_tier = 0u32 ;
14341435 let mut consecutive_data: u32 = 0 ;
14351436 let mut is_elevated = false ;
14361437 let mut total_download_bytes: u64 = 0 ;
@@ -1615,14 +1616,17 @@ async fn tunnel_loop(
16151616 if inflight. is_empty ( ) && !eof_seen {
16161617 let all_legacy = mux. all_servers_legacy ( ) ;
16171618
1618- // If all servers are legacy and we've had many consecutive
1619- // empties, wait for client data before sending.
1620- if all_legacy && consecutive_empty > 3 && !client_closed {
1619+ // After enough consecutive empties, stop polling and just
1620+ // wait for client data. Apps maintain their own heartbeats
1621+ // (MQTT PINGREQ, FCM keepalive, etc.) which trigger client
1622+ // writes that send data ops — those act as natural polls.
1623+ if ( idle_tier > 1 || ( all_legacy && consecutive_empty > 3 ) ) && !client_closed {
16211624 read_buf. reserve ( 65536 ) ;
16221625 match reader. read_buf ( & mut read_buf) . await {
16231626 Ok ( 0 ) => break ,
16241627 Ok ( n) => {
16251628 consecutive_empty = 0 ;
1629+ idle_tier = 0 ;
16261630 let data = extract_bytes ( & mut read_buf, n) ;
16271631 let ( meta, reply_rx) = send_data_op ( sid, data, & mut next_send_seq, & mut next_data_write_seq, mux) ;
16281632 inflight. push ( wrap_reply ( meta, reply_rx) ) ;
@@ -1632,17 +1636,14 @@ async fn tunnel_loop(
16321636 }
16331637 }
16341638
1635- // Escalating backoff: avoid flooding empty polls on idle
1636- // sessions. Mirrors the pre-pipelining cadence.
1637- let keepalive_delay = match consecutive_empty {
1639+ // Early backoff: first few empties still poll with delay.
1640+ let keepalive_delay = match idle_tier {
16381641 0 => Duration :: from_millis ( 20 ) ,
16391642 1 => Duration :: from_millis ( 80 ) ,
1640- 2 => Duration :: from_millis ( 200 ) ,
1641- 3 => Duration :: from_millis ( 500 ) ,
1642- _ => Duration :: from_secs ( 2 ) ,
1643+ 2 => Duration :: from_secs ( 4 ) ,
1644+ _ => Duration :: from_secs ( 10 ) ,
16431645 } ;
1644- if consecutive_empty > 0 {
1645- // Wait for either the backoff timer or client data.
1646+ if idle_tier > 0 {
16461647 if !client_closed {
16471648 read_buf. reserve ( 65536 ) ;
16481649 tokio:: select! {
@@ -1652,6 +1653,7 @@ async fn tunnel_loop(
16521653 Ok ( 0 ) => break ,
16531654 Ok ( n) => {
16541655 consecutive_empty = 0 ;
1656+ idle_tier = 0 ;
16551657 let data = extract_bytes( & mut read_buf, n) ;
16561658 let ( meta, reply_rx) = send_data_op( sid, data, & mut next_send_seq, & mut next_data_write_seq, mux) ;
16571659 inflight. push( wrap_reply( meta, reply_rx) ) ;
@@ -1745,6 +1747,7 @@ async fn tunnel_loop(
17451747 next_write_seq += 1 ;
17461748 if got_data {
17471749 consecutive_empty = 0 ;
1750+ idle_tier = idle_tier / 2 ;
17481751 consecutive_data = consecutive_data. saturating_add( 1 ) ;
17491752 let bytes = resp. d. as_ref( ) . map( |d| d. len( ) as u64 * 3 / 4 ) . unwrap_or( 0 ) ;
17501753 total_download_bytes += bytes;
@@ -1755,6 +1758,7 @@ async fn tunnel_loop(
17551758 // empty result is expected.
17561759 } else {
17571760 consecutive_empty = consecutive_empty. saturating_add( 1 ) ;
1761+ idle_tier = idle_tier. saturating_add( 1 ) ;
17581762 consecutive_data = 0 ;
17591763 }
17601764 if is_eof {
@@ -1769,6 +1773,7 @@ async fn tunnel_loop(
17691773 match write_tunnel_response( & mut writer, & buffered_resp) . await ? {
17701774 WriteOutcome :: Wrote => {
17711775 consecutive_empty = 0 ;
1776+ idle_tier = idle_tier / 2 ;
17721777 consecutive_data = consecutive_data. saturating_add( 1 ) ;
17731778 let bytes = buffered_resp. d. as_ref( ) . map( |d| d. len( ) as u64 * 3 / 4 ) . unwrap_or( 0 ) ;
17741779 total_download_bytes += bytes;
@@ -1778,6 +1783,7 @@ async fn tunnel_loop(
17781783 // Stale empty poll — don't break data streak.
17791784 } else {
17801785 consecutive_empty = consecutive_empty. saturating_add( 1 ) ;
1786+ idle_tier = idle_tier. saturating_add( 1 ) ;
17811787 consecutive_data = 0 ;
17821788 }
17831789 }
@@ -1881,6 +1887,7 @@ async fn tunnel_loop(
18811887 meta. seq,
18821888 ) ;
18831889 consecutive_empty = consecutive_empty. saturating_add( 1 ) ;
1890+ idle_tier = idle_tier. saturating_add( 1 ) ;
18841891 }
18851892 ReplyOutcome :: Dropped => {
18861893 break ;
0 commit comments