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 only

View on GitHub (pinned to e474e8803c)

Solutions

  1. Reduce the job's returned data: return only needed columns/rows, aggregate, or write large payloads to S3/object storage and return a reference.
  2. Increase the worker/workspace result-size limit env (e.g. the result/streaming limit variable) if large results are expected and memory allows.
  3. For SQL executors, add LIMIT/pagination to the query.
  4. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/9cac0fa9bbeddf38. Report an issue: GitHub.