Streaming very large XML documents
A normal transform builds the whole tree in memory. Streaming templates process a document in a single forward pass at roughly constant cost.
Why memory is the constraint
A DOM-like tree costs several times the size of the file because every element becomes an object. A 500 MB export can therefore need a few gigabytes of heap, and the usual failure is not a clean error but an out-of-memory kill during a nightly job.
- Streaming works on a forward-only pass and can see the children of one element at a time.
- It cannot go backwards: no
ancestor::, nopreceding-sibling::, and no sorting the whole document. - You cannot group across the whole document in a streaming pass. Group within the element that bounds the group.
- Streaming is a design decision, not a flag you can add to an existing stylesheet.
# Saxon streaming mode, with a heap big enough for your buffers but not the document
java -Xmx512m -cp saxon-he.jar net.sf.saxon.Transform \
-s:huge.xml -xsl:stream.xsl -o:out.html
# Measure peak memory rather than guessing
/usr/bin/time -v java -Xmx512m -cp saxon-he.jar net.sf.saxon.Transform \
-s:huge.xml -xsl:stream.xsl -o:out.html 2>&1 | grep MaximumWriting a streamable stylesheet
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:mode streamable="yes" on-no-match="shallow-skip"/>
<!-- Handle the repeating record with a match on the element itself.
Everything below it is in memory; everything above it is streamed past. -->
<xsl:template match="record">
<li>
<xsl:value-of select="id"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="title"/>
</li>
</xsl:template>
<xsl:template match="feed">
<ul><xsl:apply-templates select="record"/></ul>
</xsl:template>
</xsl:stylesheet><!-- xsl:iterate keeps state forward, which a stream cannot do with recursion -->
<xsl:iterate select="record">
<xsl:param name="total" select="0"/>
<xsl:on-completion><p>Total: <xsl:value-of select="$total"/></p></xsl:on-completion>
<xsl:next-iteration>
<xsl:with-param name="total" select="$total + number(price)"/>
</xsl:next-iteration>
</xsl:iterate>⚠️
Streaming has subtle rules: an expression that reaches outside the current element's subtree is a compile-time error, and
xsl:fork is the only way to make two independent passes over one subtree. Do not rewrite a working transform for streaming unless a measurement says the memory actually hurts.FAQ
Should I stream or pre-split the file?
Pre-splitting with a streaming-friendly tool such as a SAX script or split on the record element is usually simpler, because each part then runs through an ordinary in-memory transform. Stream only when the record boundary cannot be found externally.
Which engines support streaming?
Saxon-EE supports it fully, and Saxon-HE supports a useful subset. libxslt, Xalan and .NET have no streaming XSLT mode at all.
Related
XSLT 2.0 and 3.0 beyond version 1.0 Debugging and testing stylesheets
Last refreshed 2026-09-18.