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::, no preceding-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 Maximum

Writing 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.

XSLT 2.0 and 3.0 beyond version 1.0 Debugging and testing stylesheets

Last refreshed 2026-09-18.