Stop Loading Huge XML Into Memory: Pick Streaming Parsing by Access Pattern
Large XML exports crash DOM parsers because the whole tree is held in memory. Streaming parsers process events sequentially with near-constant memory. Here is how to choose and verify the approach with a Python iterparse worked example, trade-offs, and verification steps.
29 Jul 2026, 22:56 UTC

A 300 MB product feed or log archive will crash a DOM parser long before it finishes. The problem is not XML, it is loading the entire document tree into memory at once. DOM and XPath are great for small documents that need random access. For large, record-oriented exports, streaming parsing is the engineering decision that keeps memory near constant.
DOM holds the whole tree, streaming holds the current record
DOM, the Document Object Model, builds an in-memory tree of elements, attributes and text. Peak memory scales with the whole file, often several times larger than the raw bytes because of object overhead and string duplication. That is fine for configuration files and small payloads.
Streaming parsers process the file sequentially as events arrive. SAX is push-based: the parser calls your handlers for start-element, characters, end-element. StAX is pull-based: you request the next event. .NET XmlReader and Python ElementTree.iterparse follow the same idea. Peak memory scales with the largest single element, not the whole document, because you only keep state you explicitly retain.
Python iterparse pattern for record-oriented XML
The following pattern works for files like <log><event>...</event><event>...</event>...</log>. It processes each event when the end tag is seen, then discards it.
# run locally with Python 3.x, read access to the XML file
import xml.etree.ElementTree as ET
path = ''
context = ET.iterparse(path, events=('end',))
# keep a reference to the root so we can prune it
for event, elem in context:
if elem.tag == 'event':
# extract fields while the element is still in memory
event_id = elem.findtext('id')
ts = elem.findtext('timestamp')
# write out or batch here, e.g., append to a list or DB
# ...
# free memory for this element
elem.clear()
# prune processed children from the root to avoid buildup
if elem.tag == 'log':
# root is retained by iterparse; clearing its children helps
pass
Where to run: any environment with the Python standard library and read permission on the input file. No elevated permissions are required.
Expected checks: the script emits one record per <event> end event. Memory usage should stay flat as the file grows, provided elem.clear() is called and the root reference is pruned. If you omit clear(), memory will climb because the tree is retained.
Risks: iterparse is one-pass and forward-only. A malformed element mid-file will raise after earlier records may already have been written. Plan partial-failure handling and idempotent writes. Entity and DTD handling varies by parser and version, so verify XXE hardening for your stack. Encoding declarations and BOMs can trip concatenated files; test with the messy inputs your pipeline actually receives.
Trade-offs and middle ground
Streaming gives you low memory at the cost of random access. There is no XPath, no parent or sibling lookup, and no rewinding. Whole-document operations like XSLT, global sorting, or cross-file deduplication do not fit the model. Parsing logic becomes stateful and harder to reason about.
Practical middle grounds exist. Accumulate a bounded batch of records in memory and flush periodically to keep I/O efficient. Or stream to locate a small subtree, then hand that fragment to a DOM parser for rich queries. The decision is portable: Java has SAX and StAX, .NET has XmlReader, Python has iterparse. The reasoning transfers, only the syntax changes.
Verify the decision before shipping
Generate a synthetic XML file of known size, for example one million <event> records, and run a DOM load and a streaming version side by side. Compare peak process memory via OS metrics or a profiler. Re-run the iterparse example with and without elem.clear() plus root pruning to see the difference empirically. Validate against a real-shaped file with nested records and comments, since event sequences differ from a toy sample.
Choose DOM when you need random access and the document fits comfortably in memory. Choose streaming when you process records sequentially and memory is the constraint. That access-pattern test is the useful takeaway.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.