diff --git a/Marlin/Conditionals.h b/Marlin/Conditionals.h
index bb17924a643..36ba3918779 100644
--- a/Marlin/Conditionals.h
+++ b/Marlin/Conditionals.h
@@ -314,6 +314,13 @@
     #define Z_SAFE_HOMING
   #endif
 
+  /**
+   * Avoid double-negatives for enabling features
+   */
+  #if DISABLED(DISABLE_HOST_KEEPALIVE)
+    #define HOST_KEEPALIVE_FEATURE
+  #endif
+
   /**
    * MAX_STEP_FREQUENCY differs for TOSHIBA
    */
diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h
index c7f9ae095cb..bb8b8ba2651 100644
--- a/Marlin/Configuration.h
+++ b/Marlin/Configuration.h
@@ -646,6 +646,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/Marlin.h b/Marlin/Marlin.h
index 42f8af2c6e7..99176089af8 100644
--- a/Marlin/Marlin.h
+++ b/Marlin/Marlin.h
@@ -103,7 +103,11 @@ FORCE_INLINE void serialprintPGM(const char* str) {
 
 void get_command();
 
-void idle(); // the standard idle routine calls manage_inactivity(false)
+void idle(
+  #if ENABLED(FILAMENTCHANGEENABLE)
+    bool no_stepper_sleep=false  // pass true to keep steppers from disabling on timeout
+  #endif
+);
 
 void manage_inactivity(bool ignore_stepper_queue = false);
 
diff --git a/Marlin/Marlin_main.cpp b/Marlin/Marlin_main.cpp
index 7fe9786e087..2f2c557fc6c 100644
--- a/Marlin/Marlin_main.cpp
+++ b/Marlin/Marlin_main.cpp
@@ -426,6 +426,26 @@ static uint8_t target_extruder;
   int lpq_len = 20;
 #endif
 
+#if ENABLED(HOST_KEEPALIVE_FEATURE)
+
+  // States for managing Marlin and host communication
+  // Marlin sends messages if blocked or busy
+  enum MarlinBusyState {
+    NOT_BUSY,           // Not in a handler
+    IN_HANDLER,         // Processing a GCode
+    IN_PROCESS,         // Known to be blocking command input (as in G29)
+    PAUSED_FOR_USER,    // Blocking pending any input
+    PAUSED_FOR_INPUT    // Blocking pending text input (concept)
+  };
+
+  static MarlinBusyState busy_state = NOT_BUSY;
+  static millis_t next_busy_signal_ms = -1;
+  #define KEEPALIVE_STATE(n) do{ busy_state = n; }while(0)
+#else
+  #define host_keepalive() ;
+  #define KEEPALIVE_STATE(n) ;
+#endif // HOST_KEEPALIVE_FEATURE
+
 //===========================================================================
 //================================ Functions ================================
 //===========================================================================
@@ -2130,6 +2150,35 @@ void unknown_command_error() {
   SERIAL_ECHOPGM("\"\n");
 }
 
+#if ENABLED(HOST_KEEPALIVE_FEATURE)
+
+  void host_keepalive() {
+    millis_t ms = millis();
+    if (busy_state != NOT_BUSY) {
+      if (ms < next_busy_signal_ms) return;
+      switch (busy_state) {
+        case NOT_BUSY:
+          break;
+        case IN_HANDLER:
+        case IN_PROCESS:
+          SERIAL_ECHO_START;
+          SERIAL_ECHOLNPGM(MSG_BUSY_PROCESSING);
+          break;
+        case PAUSED_FOR_USER:
+          SERIAL_ECHO_START;
+          SERIAL_ECHOLNPGM(MSG_BUSY_PAUSED_FOR_USER);
+          break;
+        case PAUSED_FOR_INPUT:
+          SERIAL_ECHO_START;
+          SERIAL_ECHOLNPGM(MSG_BUSY_PAUSED_FOR_INPUT);
+          break;
+      }
+    }
+    next_busy_signal_ms = ms + 2000UL;
+  }
+
+#endif //HOST_KEEPALIVE_FEATURE
+
 /**
  * G0, G1: Coordinated movement of X Y Z E axes
  */
@@ -3219,6 +3268,8 @@ inline void gcode_G28() {
       st_synchronize();
     #endif
 
+    KEEPALIVE_STATE(IN_HANDLER);
+
     #if ENABLED(DEBUG_LEVELING_FEATURE)
       if (marlin_debug_flags & DEBUG_LEVELING) {
         SERIAL_ECHOLNPGM("<<< gcode_G29");
@@ -3325,12 +3376,16 @@ inline void gcode_G92() {
     refresh_cmd_timeout();
     if (codenum > 0) {
       codenum += previous_cmd_ms;  // wait until this time for a click
+      KEEPALIVE_STATE(PAUSED_FOR_USER);
       while (millis() < codenum && !lcd_clicked()) idle();
+      KEEPALIVE_STATE(IN_HANDLER);
       lcd_ignore_click(false);
     }
     else {
       if (!lcd_detected()) return;
+      KEEPALIVE_STATE(PAUSED_FOR_USER);
       while (!lcd_clicked()) idle();
+      KEEPALIVE_STATE(IN_HANDLER);
     }
     if (IS_SD_PRINTING)
       LCD_MESSAGEPGM(MSG_RESUMING);
@@ -4963,6 +5018,8 @@ inline void gcode_M303() {
 
   if (e >=0 && e < EXTRUDERS)
     target_extruder = e;
+
+  KEEPALIVE_STATE(NOT_BUSY);
   PID_autotune(temp, e, c);
 }
 
@@ -5412,6 +5469,7 @@ inline void gcode_M503() {
     delay(100);
     LCD_ALERTMESSAGEPGM(MSG_FILAMENTCHANGE);
     millis_t next_tick = 0;
+    KEEPALIVE_STATE(WAIT_FOR_USER);
     while (!lcd_clicked()) {
       #if DISABLED(AUTO_FILAMENT_CHANGE)
         millis_t ms = millis();
@@ -5419,9 +5477,7 @@ inline void gcode_M503() {
           lcd_quick_feedback();
           next_tick = ms + 2500; // feedback every 2.5s while waiting
         }
-        manage_heater();
-        manage_inactivity(true);
-        lcd_update();
+        idle(true);
       #else
         current_position[E_AXIS] += AUTO_FILAMENT_CHANGE_LENGTH;
         destination[E_AXIS] = current_position[E_AXIS];
@@ -5429,6 +5485,7 @@ inline void gcode_M503() {
         st_synchronize();
       #endif
     } // while(!lcd_clicked)
+    KEEPALIVE_STATE(IN_HANDLER);
     lcd_quick_feedback(); // click sound feedback
 
     #if ENABLED(AUTO_FILAMENT_CHANGE)
@@ -5765,6 +5822,8 @@ void process_next_command() {
   seen_pointer = current_command;
   codenum = code_value_short();
 
+  KEEPALIVE_STATE(IN_HANDLER);
+
   // Handle a known G, M, or T
   switch (command_code) {
     case 'G': switch (codenum) {
@@ -6286,6 +6345,8 @@ void process_next_command() {
     default: code_is_good = false;
   }
 
+  KEEPALIVE_STATE(NOT_BUSY);
+
 ExitUnknownCommand:
 
   // Still unknown command? Throw an error
@@ -6972,9 +7033,18 @@ void disable_all_steppers() {
 /**
  * Standard idle routine keeps the machine alive
  */
-void idle() {
+void idle(
+  #if ENABLED(FILAMENTCHANGEENABLE)
+    bool no_stepper_sleep/*=false*/
+  #endif
+) {
   manage_heater();
-  manage_inactivity();
+  manage_inactivity(
+    #if ENABLED(FILAMENTCHANGEENABLE)
+      no_stepper_sleep
+    #endif
+  );
+  host_keepalive();
   lcd_update();
 }
 
diff --git a/Marlin/example_configurations/Felix/Configuration.h b/Marlin/example_configurations/Felix/Configuration.h
index 6c397725f77..c9d7e970e7a 100644
--- a/Marlin/example_configurations/Felix/Configuration.h
+++ b/Marlin/example_configurations/Felix/Configuration.h
@@ -629,6 +629,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/Felix/Configuration_DUAL.h b/Marlin/example_configurations/Felix/Configuration_DUAL.h
index 674025b9dd9..6f2cffa614e 100644
--- a/Marlin/example_configurations/Felix/Configuration_DUAL.h
+++ b/Marlin/example_configurations/Felix/Configuration_DUAL.h
@@ -626,6 +626,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/Hephestos/Configuration.h b/Marlin/example_configurations/Hephestos/Configuration.h
index 8f69858bd4d..7fdec04dd8d 100644
--- a/Marlin/example_configurations/Hephestos/Configuration.h
+++ b/Marlin/example_configurations/Hephestos/Configuration.h
@@ -638,6 +638,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = true; // set to true to invert the lo
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/Hephestos_2/Configuration.h b/Marlin/example_configurations/Hephestos_2/Configuration.h
index 201a2865e7b..b8a920bc71d 100644
--- a/Marlin/example_configurations/Hephestos_2/Configuration.h
+++ b/Marlin/example_configurations/Hephestos_2/Configuration.h
@@ -641,6 +641,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/K8200/Configuration.h b/Marlin/example_configurations/K8200/Configuration.h
index 6c67ad4fb8d..45c104eb785 100644
--- a/Marlin/example_configurations/K8200/Configuration.h
+++ b/Marlin/example_configurations/K8200/Configuration.h
@@ -661,6 +661,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/RepRapWorld/Megatronics/Configuration.h b/Marlin/example_configurations/RepRapWorld/Megatronics/Configuration.h
index 9687deac0e0..8c5a6080f0c 100644
--- a/Marlin/example_configurations/RepRapWorld/Megatronics/Configuration.h
+++ b/Marlin/example_configurations/RepRapWorld/Megatronics/Configuration.h
@@ -646,6 +646,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/RigidBot/Configuration.h b/Marlin/example_configurations/RigidBot/Configuration.h
index 61d1df7fdd7..ce55421b2a4 100644
--- a/Marlin/example_configurations/RigidBot/Configuration.h
+++ b/Marlin/example_configurations/RigidBot/Configuration.h
@@ -641,6 +641,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/SCARA/Configuration.h b/Marlin/example_configurations/SCARA/Configuration.h
index ebaf29c861e..2e56626b3af 100644
--- a/Marlin/example_configurations/SCARA/Configuration.h
+++ b/Marlin/example_configurations/SCARA/Configuration.h
@@ -654,6 +654,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/TAZ4/Configuration.h b/Marlin/example_configurations/TAZ4/Configuration.h
index 7067155e5a2..b6671fa6f48 100644
--- a/Marlin/example_configurations/TAZ4/Configuration.h
+++ b/Marlin/example_configurations/TAZ4/Configuration.h
@@ -666,6 +666,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/WITBOX/Configuration.h b/Marlin/example_configurations/WITBOX/Configuration.h
index f320e6ce258..7ca638c21ac 100644
--- a/Marlin/example_configurations/WITBOX/Configuration.h
+++ b/Marlin/example_configurations/WITBOX/Configuration.h
@@ -638,6 +638,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = true; // set to true to invert the lo
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/adafruit/ST7565/Configuration.h b/Marlin/example_configurations/adafruit/ST7565/Configuration.h
index 2fb0e1e2081..705b9c39aa7 100644
--- a/Marlin/example_configurations/adafruit/ST7565/Configuration.h
+++ b/Marlin/example_configurations/adafruit/ST7565/Configuration.h
@@ -646,6 +646,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/delta/biv2.5/Configuration.h b/Marlin/example_configurations/delta/biv2.5/Configuration.h
index 65e42851260..4b15caa8ff4 100644
--- a/Marlin/example_configurations/delta/biv2.5/Configuration.h
+++ b/Marlin/example_configurations/delta/biv2.5/Configuration.h
@@ -768,6 +768,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = true; // set to true to invert the lo
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/delta/generic/Configuration.h b/Marlin/example_configurations/delta/generic/Configuration.h
index 11e7e63ff80..84dc90a3681 100644
--- a/Marlin/example_configurations/delta/generic/Configuration.h
+++ b/Marlin/example_configurations/delta/generic/Configuration.h
@@ -768,6 +768,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = true; // set to true to invert the lo
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/delta/kossel_mini/Configuration.h b/Marlin/example_configurations/delta/kossel_mini/Configuration.h
index c65f5bebd6e..e43de5a18d1 100644
--- a/Marlin/example_configurations/delta/kossel_mini/Configuration.h
+++ b/Marlin/example_configurations/delta/kossel_mini/Configuration.h
@@ -772,6 +772,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/delta/kossel_pro/Configuration.h b/Marlin/example_configurations/delta/kossel_pro/Configuration.h
index b3dfc272ff8..b0755f58102 100644
--- a/Marlin/example_configurations/delta/kossel_pro/Configuration.h
+++ b/Marlin/example_configurations/delta/kossel_pro/Configuration.h
@@ -763,6 +763,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/delta/kossel_xl/Configuration.h b/Marlin/example_configurations/delta/kossel_xl/Configuration.h
index 987e35346f6..ccda7a39ef3 100644
--- a/Marlin/example_configurations/delta/kossel_xl/Configuration.h
+++ b/Marlin/example_configurations/delta/kossel_xl/Configuration.h
@@ -682,6 +682,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/makibox/Configuration.h b/Marlin/example_configurations/makibox/Configuration.h
index 21229b29741..43536fe6ac0 100644
--- a/Marlin/example_configurations/makibox/Configuration.h
+++ b/Marlin/example_configurations/makibox/Configuration.h
@@ -649,6 +649,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = false; // set to true to invert the l
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/example_configurations/tvrrug/Round2/Configuration.h b/Marlin/example_configurations/tvrrug/Round2/Configuration.h
index 229ff16060a..a8026d77118 100644
--- a/Marlin/example_configurations/tvrrug/Round2/Configuration.h
+++ b/Marlin/example_configurations/tvrrug/Round2/Configuration.h
@@ -640,6 +640,14 @@ const bool Z_MIN_PROBE_ENDSTOP_INVERTING = true; // set to true to invert the lo
   #define EEPROM_CHITCHAT // Please keep turned on if you can.
 #endif
 
+//
+// Host Keepalive
+//
+// By default Marlin will send a busy status message to the host
+// every 2 seconds when it can't accept commands.
+//
+//#define DISABLE_HOST_KEEPALIVE // Enable this option if your host doesn't like keepalive messages.
+
 //
 // M100 Free Memory Watcher
 //
diff --git a/Marlin/language.h b/Marlin/language.h
index e0fb3d9f2e3..3637babe33f 100644
--- a/Marlin/language.h
+++ b/Marlin/language.h
@@ -124,6 +124,9 @@
 #define MSG_COUNT_A                         " Count A: "
 #define MSG_ERR_KILLED                      "Printer halted. kill() called!"
 #define MSG_ERR_STOPPED                     "Printer stopped due to errors. Fix the error and use M999 to restart. (Temperature is reset. Set it after restarting)"
+#define MSG_BUSY_PROCESSING                 "busy: processing"
+#define MSG_BUSY_PAUSED_FOR_USER            "busy: paused for user"
+#define MSG_BUSY_PAUSED_FOR_INPUT           "busy: paused for input"
 #define MSG_RESEND                          "Resend: "
 #define MSG_UNKNOWN_COMMAND                 "Unknown command: \""
 #define MSG_ACTIVE_EXTRUDER                 "Active Extruder: "