Large Scale XML Sitemaps in PHP: Streaming, Sharding & Index Files

Santaji GadePHP2 days ago7 Views

Large Scale XML Sitemaps

A large-scale XML sitemap in PHP needs streaming, not fetchAll — XMLWriter, unbuffered PDO, and automatic sharding at the 50,000 URL wall.

Technical SEO XML Sitemaps PHP 2026

An XML sitemap for a large site in PHP hits a wall that small-site tutorials never mention: the 50,000 URL and 50MB-uncompressed limits defined in the Sitemaps protocol. Once a site crosses that line, building one flat sitemap file isn't just wrong, it can produce a file search engines silently reject or truncate.

01Large Scale XML Sitemaps in PHP: Why the Approach Changes

The official Sitemaps protocol is explicit: each sitemap file must contain no more than 50,000 URLs and must be no larger than 50MB uncompressed. Cross either limit, and you need multiple sitemap files referenced by a single sitemap index file instead of one giant document.

The second wall is memory, not the URL protocol. Jasmine Directory's guide is direct about the fix: use XMLWriter for streaming generation and PDO with unbuffered queries, and avoid loading entire result sets into arrays. Stream everything, don't build the whole document in memory first.

50,000
maximum URLs allowed per individual sitemap file under the protocol
50MB
maximum uncompressed file size per sitemap, gzip doesn't change this cap
50,000
maximum sitemap files a single index file may reference
Advertisement
Advertisement

02Streaming a Sitemap File With XMLWriter

Instead of building a DOM object in memory and serializing it at the end, PHP's XMLWriter class writes elements incrementally, directly to a file or output stream, keeping memory usage flat regardless of how many URLs you write.

Streaming a Single Sitemap File (Memory-Flat)
function writeSitemapFile($filePath, $urlGenerator) {
  $writer = new XMLWriter();
  $writer->openUri($filePath);
  $writer->startDocument('1.0', 'UTF-8');
  $writer->startElement('urlset');
  $writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');

  // $urlGenerator is a PHP generator, one row is held in memory at a time
  foreach ($urlGenerator as $row) {
    $writer->startElement('url');
    $writer->writeElement('loc', $row['url']);
    $writer->writeElement('lastmod', $row['updated_at']);
    $writer->endElement(); // 
  }

  $writer->endElement(); // 
  $writer->endDocument();
  $writer->flush();
}

03Feeding It From an Unbuffered PDO Query

The writer above expects a generator, not an array. PDO's MySQL driver buffers full result sets in memory by default, defeating the point of streaming. Disabling buffered queries and wrapping the cursor in a PHP generator keeps only one row in memory at any time, regardless of table size.

Unbuffered PDO Query as a Generator
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

function fetchIndexableUrls($pdo) {
  $stmt = $pdo->query(
    'SELECT url, updated_at FROM pages WHERE noindex = 0 ORDER BY id'
  );

  // yield hands over one row and pauses, this is the memory-saving part
  while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    yield $row;
  }
}

writeSitemapFile('sitemaps/sitemap-1.xml', fetchIndexableUrls($pdo));
Quick Tip

Only indexable, published pages belong in the query. Unpublish a page and forget to exclude it here, and you're actively asking search engines to crawl and index content you no longer want in results, which wastes crawl budget on exactly the pages that matter least.

Advertisement
Advertisement

04Sharding Into Multiple Files at the 50,000 Mark

Once total URLs exceed 50,000, the generator needs to close the current file and open a new one automatically, rather than one script writing a single oversized file that violates the protocol.

Automatic Sharding at the URL Limit
function generateShardedSitemaps($urlGenerator, $outputDir, $limitPerFile = 50000) {
  $fileIndex = 1;
  $count = 0;
  $writer = null;
  $generatedFiles = [];

  foreach ($urlGenerator as $row) {
    if ($count % $limitPerFile === 0) {
      if ($writer) {
        $writer->endElement();
        $writer->endDocument();
        $writer->flush();
      }
      $fileName = "sitemap-{$fileIndex}.xml";
      $generatedFiles[] = $fileName;
      $writer = new XMLWriter();
      $writer->openUri($outputDir . '/' . $fileName);
      $writer->startDocument('1.0', 'UTF-8');
      $writer->startElement('urlset');
      $writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
      $fileIndex++;
    }

    $writer->startElement('url');
    $writer->writeElement('loc', $row['url']);
    $writer->writeElement('lastmod', $row['updated_at']);
    $writer->endElement();

    $count++;
  }

  if ($writer) {
    $writer->endElement();
    $writer->endDocument();
    $writer->flush();
  }

  return $generatedFiles;
}

05Building the Sitemap Index File

Once URLs are split across multiple files, a single sitemap index file lists each one. This is the only file you submit to Search Console and reference in robots.txt, search engines follow it to discover the rest.

Generating the Sitemap Index File
function writeSitemapIndex($generatedFiles, $baseUrl, $outputPath) {
  $writer = new XMLWriter();
  $writer->openUri($outputPath . '/sitemap-index.xml');
  $writer->startDocument('1.0', 'UTF-8');
  $writer->startElement('sitemapindex');
  $writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');

  foreach ($generatedFiles as $fileName) {
    $writer->startElement('sitemap');
    $writer->writeElement('loc', $baseUrl . '/' . $fileName);
    $writer->writeElement('lastmod', date('c'));
    $writer->endElement();
  }

  $writer->endElement();
  $writer->endDocument();
  $writer->flush();
}
🔎 Did you know?

A documented real-world case handled 60 million URLs across 20 languages in PHP and Laravel. The team's original approach kept every processed item in memory until hitting the 50,000 limit, which meant items sat in RAM the whole time. Switching to write-and-stream in small batches, opening and closing the sitemap file as needed, was what actually made the process viable at that scale.

Advertisement
Advertisement

06Gzip Compression Without Blowing Up Memory

Letter Counter's guide notes gzip compression can reduce actual transfer size by 70-90% without affecting how the 50MB limit is calculated, since the cap applies to the uncompressed content. For large sitemaps, compress the already-written file rather than trying to gzip inline during the write loop.

Gzip Compress an Already-Written Sitemap File
function gzipSitemapFile($filePath) {
  $gzPath = $filePath . '.gz';
  $source = fopen($filePath, 'rb');
  $dest = gzopen($gzPath, 'wb9');

  while (!feof($source)) {
    gzwrite($dest, fread($source, 1048576)); // 1MB chunks
  }

  fclose($source);
  gzclose($dest);
}

07Buffered vs Streaming Approach Compared

A quick reference for why the streaming approach matters as URL counts grow.

FactorBuffered (fetchAll + DOMDocument)Streaming (Generator + XMLWriter)
Memory usage at 1M URLsScales with total URL countStays flat regardless of total count
Risk of memory limit crashHigh on large sitesEffectively eliminated
Code complexitySimpler to write initiallySlightly more setup, generators + sharding
Suitable site sizeSmall sites, under a few thousand URLsAny size, including tens of millions

08Implementation Checklist

A short list to confirm before deploying a large-scale sitemap generator.

Use XMLWriter, not DOMDocument, for anything beyond a few thousand URLs, to keep memory flat.

Disable PDO query buffering, wrap the cursor in a generator so only one row loads at a time.

Shard automatically at 50,000 URLs, don't rely on manual file splitting.

Generate a sitemap index file, this is the single file you submit and reference in robots.txt.

Exclude noindexed and unpublished content from the source query, don't waste crawl budget on it.

09Common Questions

It violates the Sitemaps protocol and may be rejected or truncated by search engines. Split URLs across multiple files, each under the 50,000 limit, and reference them all from a single sitemap index file.

No. The 50MB cap applies to the uncompressed file size, regardless of how small the compressed version ends up being. Gzip saves bandwidth, it doesn't raise the limit.

Usually because the entire URL set is loaded into an array or DOMDocument before writing. Switch to a PHP generator feeding an XMLWriter stream to keep memory usage flat regardless of total URL count.

Submit the sitemap index file. Search engines follow it to discover and crawl each individual sitemap file automatically, so you only need to reference one URL in Search Console and robots.txt.

Not by default. MySQL's PDO driver buffers full result sets in memory. Disable this with PDO::MYSQL_ATTR_USE_BUFFERED_QUERY set to false, and wrap the fetch loop in a generator using yield.

What We Learn Today

The protocol caps every sitemap at 50,000 URLs and 50MB uncompressed

XMLWriter streams output instead of building a DOM in memory

Unbuffered PDO queries plus generators keep memory flat at any scale

Sharding must happen automatically once the URL count crosses the limit

A sitemap index file is the single file you submit and reference

Gzip cuts transfer size but never changes the uncompressed size limit

Build a Complete PHP Technical SEO Toolkit

Large-scale sitemap generation pairs naturally with dynamic meta tags and sitemap type comparisons. Explore both guides next.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...