Santaji GadePHP2 days ago7 Views

A large-scale XML sitemap in PHP needs streaming, not fetchAll — XMLWriter, unbuffered PDO, and automatic sharding at the 50,000 URL wall.
Table of Contents
ToggleAn 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.
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.
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.
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(); }
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.
$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));
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.
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.
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; }
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.
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(); }
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.
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.
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); }
A quick reference for why the streaming approach matters as URL counts grow.
| Factor | Buffered (fetchAll + DOMDocument) | Streaming (Generator + XMLWriter) |
|---|---|---|
| Memory usage at 1M URLs | Scales with total URL count | Stays flat regardless of total count |
| Risk of memory limit crash | High on large sites | Effectively eliminated |
| Code complexity | Simpler to write initially | Slightly more setup, generators + sharding |
| Suitable site size | Small sites, under a few thousand URLs | Any size, including tens of millions |
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.
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.
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
Large-scale sitemap generation pairs naturally with dynamic meta tags and sitemap type comparisons. Explore both guides next.









