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
- Reduce the size of the returned payload: project/aggregate data in the task so only what n8n needs is sent over the pipe.
- Increase task_timeout so the reader has headroom to drain large legitimate payloads after the child exits.
- Check for prior kill/cancel events in the same execution that could have left a truncated pipe message; reproduce with a clean run.
- 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
- Cap the size of returned payloads in user code.
- Set task_timeout with headroom for serialization of the largest expected result.
- Avoid returning deeply nested or recursive structures that serialize slowly.
- Monitor pipe read latency to detect drift.
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
- Task timeout must be positive, got {task_timeout}
- Auto shutdown timeout must be non-negative, got {auto_shutdo
- Graceful shutdown timeout must be positive, got {graceful_sh
- Task execution timed out after {task_timeout} seconds
- Process completed but returned no result. This is likely an
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/20d00196232562e7.
Report an issue: GitHub.