Workflow performance
How I reduced memory pressure in my AI agent workflows
How I reduced AI agent memory pressure with Typst, Rust, and streaming CSV workflows. Compare measured runtime and peak memory in a million-row benchmark.
I have a 36 GB MacBook Pro. I was still frequently running out of memory.
I was working across Codex and Antigravity. The agents were spawning multiple Chrome instances for website browsing and document generation. I was also using Python to process large datasets, including county property-assessment CSVs.
So we started changing the tools the agents used. Document generation moved to Typst. Large-dataset processing moved toward Rust. We created skills, rewrote shell commands, added aliases, and searched GitHub and Homebrew for existing replacements.
Then we added a daily job to review agent sessions, tool calls, and errors—and apply fixes automatically.
Move document generation out of the browser
Document generation was one place to remove a browser from the workflow. I moved that work to Typst.
Typst has a local compiler and command-line interface. A document can be compiled to PDF without launching Chrome. Its official documentation shows the basic command:
typst compile report.typ report.pdf
The agents could use a document compiler instead of a browser-based generation flow.
That doesn’t eliminate the need to browse websites. It separates document generation from work that actually needs a browser.
Move large CSV processing from Python to Rust
County assessment CSVs were a concrete example of the data-processing side.
I had been processing them with Python. Moving that work to Rust was much faster and used less memory in my experience.
Speed and memory use were both reasons for the change.
Those observations are specific to my assessment-data workflow, not a general Python-versus-Rust benchmark.
Put numbers behind the CSV workflow
To measure the CSV side, we built a separate benchmark. We used synthetic assessment-style data, not actual property records or a replay of my earlier migration.
The larger file had 1,000,000 rows and was 159.46 MB. Python and Rust both read and parsed it, validated the numeric fields and totals, calculated aggregates, and hashed the parsed content with SHA-256. Every measured run produced matching results.
We tested two ways of processing the file in each language: streaming one row at a time, and loading all the decoded rows into memory before processing them.
Here are the rounded results for the million-row file. Each number is the median of five fresh-process runs:
| Implementation | Time | Peak process memory |
|---|---|---|
| Python, streaming | 3.3 s | 19 MiB |
| Rust, streaming | 1.0 s | 6.2 MiB |
| Python, all rows buffered | 4.8 s | 846 MiB |
| Rust, all rows buffered | 1.1 s | 385 MiB |
Comparing streaming with streaming, Rust was 3.3× faster and used 68% less peak process memory. Comparing buffered with buffered, Rust was 4.6× faster and used 55% less peak process memory. These ratios compare matching approaches, not the lightest Rust version against the heaviest Python version.
The graphs show the same rounded medians. Comparisons use the unrounded measurements.
The smaller, 169,727-row file was 26.97 MB. Streaming Python took 0.543 s versus Rust’s 0.160 s, with peak memory of 19.48 MiB versus 6.20 MiB. Buffered Python took 0.655 s versus Rust’s 0.175 s, with peak memory of 159.89 MiB versus 70.16 MiB.
What these numbers measure
We ran this on an Apple M3 Pro MacBook Pro with 36 GB of memory, macOS 26.6.1, Python 3.12.14, and Rust 1.98.1. Rust used an optimized release build and the csv crate, version 1.4.0. Python used its standard-library CSV parser, which is C-backed—not a hand-written Python parser.
There was one excluded warmup for each implementation and file size, followed by five measurements. The file cache was warm, and normal applications stayed running. Times include process startup, file reading, parsing, validation, aggregation, and hashing. They exclude compilation and fixture generation.
Times varied between runs. On the million-row file, streaming Python ranged from about 3.2–5.0 s, versus 0.9–1.6 s for Rust. Buffered runs ranged from about 4.7–8.7 s for Python and 1.0–1.2 s for Rust. These are observed ranges, not confidence intervals.
Peak process memory is the maximum resident set size reported by macOS for each child process, converted to MiB. It is measured memory, not an estimate from object sizes. It is not total Mac memory usage.
This test does not measure database import, Chrome, Typst, document generation, or model calls. It also doesn’t verify the roughly 10× improvement I noticed during the earlier changes. It measures these implementations of this synthetic workload on this machine.
Download all raw benchmark measurements, including the excluded warmups, fixture hashes, and environment details. Only synthetic data was used; local file paths have been removed from the download.
Change the skills and commands the agents use
We changed more than the CSV parser.
We created new skills, rewrote shell commands, and added shell aliases. We pointed the tooling toward Rust alternatives so those choices were part of the agents’ normal workflow.
I also told the agents to look for existing packages and replacements on GitHub and Homebrew. Finding a tool that already handles the job was part of the process, not just rewriting everything ourselves.
The changes covered both the tools and the instructions for using them: what to run, what to search for, and which alternatives to prefer.
Add an automatic daily review
We added a daily scheduled job to analyze agent sessions, tool calls, and errors. It’s configured to apply fixes or redirect work toward Rust alternatives automatically.
Rust helped. Streaming mattered too.
The benchmark gives the CSV part of this story specific numbers: 3.3× faster and 68% less peak memory for the Rust streaming implementation compared with the Python streaming implementation.
It also shows why the way a tool handles data matters. Keeping the language the same and switching to streaming reduced Python’s peak memory from about 846 MiB to 19 MiB—about 98%. Rust went from about 385 MiB to 6.2 MiB, also about 98%.
The useful comparison isn’t just Python versus Rust. It’s also how much of the file the tool keeps in memory. The document change follows a similar practical idea: don’t launch a browser when a document compiler can do the job.
I changed what the agents ran. Then we changed the instructions and tooling that shaped their next choices.