n8n-io/n8n · error · TaskResultReadError

Failed to read result from child process

Error message

Failed to read result from child process

What it means

TaskResultReadError wraps a TimeoutError raised when pipe_reader.is_alive() is still true after pipe_reader.join(timeout=task_timeout). The child process finished (it was not alive at the earlier join), but the reader thread that drains the result pipe did not finish reading within the same ceiling. The read connection is force-closed before raising.

Source

Thrown at packages/@n8n/task-runner-python/src/task_executor.py:250

            if process.exitcode == SIGTERM_EXIT_CODE:
                raise TaskCancelledError()

            if process.exitcode == SIGKILL_EXIT_CODE:
                raise TaskKilledError()

            if process.exitcode != 0:
                assert process.exitcode is not None
                raise TaskSubprocessFailedError(process.exitcode)

            pipe_reader.join(timeout=task_timeout)

            if pipe_reader.is_alive():
                try:
                    read_conn.close()
                except Exception:
                    pass
                raise TaskResultReadError(
                    TimeoutError(f"Pipe reader timed out after {task_timeout}s")
                )

            if pipe_reader.error:
                raise TaskResultReadError(pipe_reader.error)

            if pipe_reader.pipe_message is None:
                raise TaskResultMissingError()

            returned = pipe_reader.pipe_message

            if "error" in returned:
                error_msg = cast(PipeErrorMessage, returned)
                raise TaskRuntimeError(error_msg["error"])

            if "result" not in returned:
                raise TaskResultMissingError()

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Reduce the size of the returned payload: project/aggregate data in the task so only what n8n needs is sent over the pipe.
  2. Increase task_timeout so the reader has headroom to drain large legitimate payloads after the child exits.
  3. Check for prior kill/cancel events in the same execution that could have left a truncated pipe message; reproduce with a clean run.
  4. Verify the runner host is not CPU-throttled or starved during result serialization.

Example fix

// before
return {'result': huge_dataframe.to_dict('records')}  # MBs over pipe
// after
summary = huge_dataframe[['id','status']].head(1000).to_dict('records')
return {'result': summary}
Defensive patterns

Strategy: validation

Validate before calling

# Estimate result size before returning.
import json
def safe_return(value, max_bytes):
    encoded = json.dumps(value, default=str).encode('utf-8')
    if len(encoded) > max_bytes:
        return {'result': {'error': 'result too large', 'size': len(encoded)}}
    return {'result': value}

Try / catch

from n8n_task_runner.errors import TaskResultReadError
try:
    result = TaskExecutor.execute_process(proc, rc, wc, task_timeout, continue_on_fail=False)
except TaskResultReadError as e:
    # reader could not drain in time; reduce payload or raise timeout
    return [{"json": {"error": f"result read failed: {e}"}}], [], 0

Prevention

When it happens

Trigger: The child wrote a very large result over the pipe (multi-MB JSON) and the reader thread could not drain it within task_timeout even though the child had exited; or the reader thread is blocked on a partial message because the child was killed mid-write; or the OS pipe buffer is saturated and the reader is slow.

Common situations: User code returns a huge array/dataframe that must be serialized and pushed through the pipe; the runner runs on a slow/CPU-throttled node so deserialization lags; an earlier TaskKilledError path left a half-written message that the reader keeps trying to parse.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/20d00196232562e7. Report an issue: GitHub.