CoplayDev/unity-mcp · warning · TimeoutError

Unity sent heartbeat frames without payload within configure

Error message

Unity sent heartbeat frames without payload within configured threshold

What it means

Raised in the framed receive loop (unity_connection.py:193). Unity emits zero-length frames as heartbeats while a long-running command is still executing. If only heartbeats arrive past max_heartbeat_frames (default 16) or heartbeat_timeout seconds (default 2.0s) without a real payload, the command is treated as wedged and a TimeoutError is raised.

Source

Thrown at Server/src/transport/legacy/unity_connection.py:193

        if self.use_framing:
            # Heartbeat semantics: the Unity editor emits zero-length frames while
            # a long-running command is still executing. We tolerate a bounded
            # number of these frames (or a small time window) before surfacing a
            # timeout to the caller so tools can retry or fail gracefully.
            heartbeat_limit = getattr(config, 'max_heartbeat_frames', 16)
            heartbeat_window = getattr(config, 'heartbeat_timeout', 2.0)
            heartbeat_started = time.monotonic()
            heartbeat_count = 0
            try:
                while True:
                    header = self._read_exact(sock, 8)
                    payload_len = struct.unpack('>Q', header)[0]
                    if payload_len == 0:
                        heartbeat_count += 1
                        logger.debug(
                            f"Received heartbeat frame #{heartbeat_count}")
                        if heartbeat_count >= heartbeat_limit or (time.monotonic() - heartbeat_started) > heartbeat_window:
                            raise TimeoutError(
                                "Unity sent heartbeat frames without payload within configured threshold"
                            )
                        continue
                    if payload_len > FRAMED_MAX:
                        raise ValueError(
                            f"Invalid framed length: {payload_len}")
                    payload = self._read_exact(sock, payload_len)
                    logger.debug(
                        f"Received framed response ({len(payload)} bytes)")
                    return payload
            except socket.timeout as exc:
                logger.warning("Socket timeout during framed receive")
                raise TimeoutError("Timeout receiving Unity response") from exc
            except TimeoutError:
                raise
            except Exception as exc:
                logger.error(f"Error during framed receive: {exc}")
                raise

View on GitHub (pinned to c21bf496bc)

Solutions

  1. Check Unity for a blocking modal dialog or a hung operation and dismiss/cancel it, then retry.
  2. If the operation is legitimately long, raise the budget via config.max_heartbeat_frames and/or config.heartbeat_timeout.
  3. Restart the Unity editor / recompile if a handler appears truly deadlocked.

Example fix

// before — default budget (16 frames / 2.0s)
# config.max_heartbeat_frames = 16

// after — allow longer-running commands
from core.config import config
config.heartbeat_timeout = 10.0
config.max_heartbeat_frames = 80
Defensive patterns

Strategy: validation

Validate before calling

from core.config import config

def heartbeat_budget_adequate(estimated_seconds: float) -> bool:
    return (config.heartbeat_timeout >= estimated_seconds
            and config.max_heartbeat_frames >= int(estimated_seconds / 0.1))

Type guard

def is_heartbeat_timeout(e: BaseException) -> bool:
    return (isinstance(e, TimeoutError)
            and 'heartbeat frames without payload' in str(e))

Try / catch

try:
    resp = conn.send_command(cmd, params)
except TimeoutError as e:
    if 'heartbeat frames without payload' in str(e):
        # likely a blocking dialog / stuck op in Unity; bump budget and retry once
        config.heartbeat_timeout = max(config.heartbeat_timeout, 10.0)
        resp = conn.send_command(cmd, params)
    else:
        raise

Prevention

When it happens

Trigger: A tool whose Unity-side handler never returns — an infinite loop, a deadlock, or a blocking modal dialog in the editor — so Unity keeps sending heartbeats but never the result frame.

Common situations: Unity showing a blocking modal/input dialog, a PlayMode script stuck in a loop, or a genuinely heavy operation that legitimately exceeds the small default heartbeat budget.

Related errors


AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13). Data as JSON: /api/errors/226fc6670848328e. Report an issue: GitHub.