windmill-labs/windmill · error
result over budget
Error message
result over budget
What it means
The result-streaming writer for job results enforces a byte budget: write_all into an in-memory Vec buffer checks the incoming chunk against the remaining allowance (self.left) and fails with io::ErrorKind::WriteZero and the message "result over budget" when exceeded. This caps how large a job result may be serialized into memory before it is sent to storage.
Source
Thrown at backend/windmill-worker/src/worker.rs:1669
/// fail for any other reason, which is what makes that reading unambiguous.
pub(crate) fn to_raw_value_within<T: serde::Serialize>(
value: &T,
budget: usize,
) -> Option<Box<serde_json::value::RawValue>> {
struct Budgeted {
buf: Vec<u8>,
left: usize,
}
impl std::io::Write for Budgeted {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.write_all(bytes)?;
Ok(bytes.len())
}
// `Vec<u8>` overrides this too: the default implementation loops over
// `write`, and serde_json emits a great many small pieces per row.
fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
if bytes.len() > self.left {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"result over budget",
));
}
self.left -= bytes.len();
self.buf.extend_from_slice(bytes);
Ok(())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut writer = Budgeted { buf: Vec::new(), left: budget };
serde_json::to_writer(&mut writer, value).ok()?;
let json = String::from_utf8(writer.buf).ok()?;
// SAFETY: `to_writer` returned `Ok`, so `json` holds one complete, well-formed
// JSON value with no surrounding whitespace. Running out of budget is the onlyView on GitHub (pinned to e474e8803c)
Solutions
- Reduce the job's returned data: return only needed columns/rows, aggregate, or write large payloads to S3/object storage and return a reference.
- Increase the worker/workspace result-size limit env (e.g. the result/streaming limit variable) if large results are expected and memory allows.
- For SQL executors, add LIMIT/pagination to the query.
- Check preceding flow steps for accidental pass-through of full outputs.
Example fix
// before return all_rows; // serialized size exceeds budget // after let limited = all_rows.into_iter().take(10_000).collect(); return limited; // or upload to storage and return the URL
Defensive patterns
Strategy: validation
Validate before calling
// Estimate the serialized size of a job result before returning it
fn fits_result_budget<T: serde::Serialize>(value: &T, budget: usize) -> bool {
serde_json::to_vec(value).map(|b| b.len() <= budget).unwrap_or(false)
} Try / catch
match write_result(&mut writer, &value) {
Err(e) if e.kind() == std::io::ErrorKind::WriteZero => {
// result over budget: fall back to storing the payload in object storage
let url = upload_to_storage(&value)?;
write_result(&mut writer, &url)?;
}
other => other?,
} Prevention
- Cap returned rows/columns with LIMIT and projections in SQL jobs
- Offload large payloads to S3/workspace storage and return references
- Configure the worker result-size limit to match expected workloads
- In flows, avoid pass-through of full step outputs between steps
When it happens
Trigger: A job returns a result whose serialized JSON exceeds the configured result size limit (e.g. MAX_SQL_RESULT_SIZE-style budget) while worker.rs streams the result into the buffer via write_all.
Common situations: Scripts returning huge arrays/dataframes, SQL jobs selecting unbounded rows, flow step outputs carrying full datasets instead of references, or an under-configured result limit for a legitimately large workload.
Related errors
- preprocessor function is missing
- ${main_name} function is missing
- No response body for SSE stream
- AI response was empty
- The connection check did not run. Is a worker listening to t
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/9cac0fa9bbeddf38.
Report an issue: GitHub.