Stream Code Execution Output in Real Time
Stream code execution output in real time over SSE — pass on_stdout/on_stderr callbacks to exec_stream and get a build's output as it's produced.
Long-running commands — a multi-minute build, a training loop, a script that prints progress — shouldn't stay silent until they exit. sandbox.exec() buffers everything and hands you the full output only after the process finishes. For anything that takes more than a second or two, you want the output as it lands: that's exec_stream (Python) / execStream (TypeScript).
Under the hood this is streaming code execution output over real-time SSE: the agent on the host forwards the process's stdout/stderr line-by-line as Server-Sent Events, and the SDK invokes your callbacks for each chunk. You never touch the SSE wire format — you just pass on_stdout/on_stderr and get the exit code back when the process ends.
exec vs. exec_stream
exec | exec_stream | |
|---|---|---|
| Output delivery | Buffered, returned at exit | Streamed via callbacks as produced |
| Return value | ExecResult (.stdout, .stderr, .exit_code, .duration_ms) | int exit code |
| Best for | Short commands, when you need the full string | Long jobs, live progress, log tailing |
| Transport | Request/response | SSE (Server-Sent Events) |
Reach for plain exec when the command is quick and you want the whole result as a string. Reach for exec_stream when you want to watch the command run.
Stream a build
Here's a real example: install dependencies and run a build, printing every line as it happens. The callback fires once per output chunk; the call returns the process exit code.
import shlex
import sys
from pandastack import Sandbox
# PANDASTACK_API_KEY is read from the environment.
sandbox = Sandbox.create(template="code-interpreter")
try:
# Clone, install, build — output streams live instead of blocking until exit.
script = (
"set -e; "
"echo '== installing =='; pip install --quiet rich; "
"echo '== building =='; "
"for i in $(seq 1 5); do echo \"step $i/5\"; sleep 1; done; "
"echo '== done =='"
)
exit_code = sandbox.exec_stream(
f"bash -lc {shlex.quote(script)}",
on_stdout=lambda chunk: print(chunk, end="", flush=True),
on_stderr=lambda chunk: print(chunk, end="", file=sys.stderr, flush=True),
timeout_seconds=300,
)
print(f"\nbuild exited with code {exit_code}")
if exit_code != 0:
raise SystemExit(exit_code)
finally:
sandbox.kill()import { Sandbox } from "@pandastack/sdk";
// PANDASTACK_API_KEY is read from the environment.
const sandbox = await Sandbox.create({ template: "code-interpreter" });
try {
// Clone, install, build — output streams live instead of blocking until exit.
const script = [
"set -e",
"echo '== installing =='; pip install --quiet rich",
"echo '== building =='",
"for i in $(seq 1 5); do echo \"step $i/5\"; sleep 1; done",
"echo '== done =='",
].join("; ");
const exitCode = await sandbox.execStream(`bash -lc ${JSON.stringify(script)}`, {
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk),
timeoutSeconds: 300,
});
console.log(`\nbuild exited with code ${exitCode}`);
if (exitCode !== 0) process.exit(exitCode);
} finally {
await sandbox.kill();
}Both calls block until the process exits, but your callbacks have already printed every line by then. The return value is just the exit code — there's no buffered .stdout to read, because you consumed it as it streamed.
How the callbacks work
| Argument | Type | Fires when |
|---|---|---|
on_stdout / onStdout | (chunk: str) -> None | A chunk of standard output is produced |
on_stderr / onStderr | (chunk: str) -> None | A chunk of standard error is produced |
timeout_seconds / timeoutSeconds | number | Kills the command and ends the stream if exceeded |
A few things to keep in mind:
- Chunks, not lines. A callback may receive a partial line or several lines at once depending on how the process flushes. If you need clean line boundaries, buffer in your callback and split on
\nyourself. - stdout and stderr stay separate. They arrive on distinct callbacks so you can route them differently (e.g. progress to stdout, warnings to a log).
- The return value is the exit code. Non-zero means the command failed; check it the way you would
ExecResult.exit_codefromexec. - Timeouts apply to the whole command, not to gaps between chunks. If the job exceeds
timeout_seconds, the stream ends and the command is killed.
Capturing while streaming
If you want both live output and the full text at the end, accumulate it in the callback:
buf = []
exit_code = sandbox.exec_stream(
"python -u long_task.py",
on_stdout=lambda c: (buf.append(c), print(c, end="", flush=True)),
)
full_output = "".join(buf)const buf: string[] = [];
const exitCode = await sandbox.execStream("python -u long_task.py", {
onStdout: (c) => {
buf.push(c);
process.stdout.write(c);
},
});
const fullOutput = buf.join("");Tip: Run Python with
-u(or setPYTHONUNBUFFERED=1) inside the sandbox. Python buffers stdout when it's not attached to a TTY, which can make progress arrive in one burst at the end.-uforces line-buffered output so your callbacks fire as each line prints.
When to use the code interpreter instead
exec_stream is for shell commands and scripts where you care about live logs. If instead you're running Python cells and want structured results — return values, DataFrames, matplotlib charts captured as PNGs — use a code interpreter context (create_code_context / createCodeContext). That path gives you Execution.results with rich outputs rather than a raw text stream. The two are complementary: stream a pip install or a build with exec_stream, then run analysis cells in a CodeContext.
For the buffered request/response model and the full ExecResult shape, see the exec concept.
Next steps
- Execute a Python API call in a sandbox — buffered
exec/run_codefor short jobs. - Install packages in a sandbox —
pip/uv/npminside the VM, often worth streaming. - Code interpreter guide — stateful Python cells with rich, structured results.
- exec concept — the underlying command-execution model and
ExecResultfields.
Execute Python in an isolated microVM via one API call
How to execute Python in an isolated microVM with one API call — create a Firecracker sandbox, run code, and read stdout from the Python or TypeScript SDK or raw curl.
Install Python packages inside a sandbox on the fly
Install Python packages inside a sandbox on the fly with pip or uv — via exec or a code-interpreter kernel — and reuse them in the next run.