Slow Stamping on Very Large PDFs
Stamping every page of a very large PDF in one pass gets progressively slower as the job runs. In the reported 20,000-page case, the bottleneck was the method used to split the document, not the stamping operation itself.
Engineering traced the slowdown in the 20,000-page case to the document-splitting method rather than the stamping operation. The detailed mechanism has not been independently confirmed from developer notes.
The fix is to split the PDF recursively into small chunks, stamp each chunk (optionally in parallel worker processes), and merge the results back into one file.
Solution
1. Split the Source PDF Recursively
Recommended: keep splitting until each chunk is at or below a target page count. Save each chunk to disk so memory stays low and workers can pick up files independently. The file name stores the chunk's first page, which keeps chunks in order and supports page-dependent stamps later.
using IronPdf;
using IronPdf.Editing;
static void SplitToFiles(PdfDocument doc, int startPage, int targetPages,
string outDir, List<string> files)
{
if (doc.PageCount <= targetPages)
{
// File name carries the chunk's first page (1-based).
string path = Path.Combine(outDir, $"chunk_{startPage:D6}.pdf");
doc.SaveAs(path);
files.Add(path);
return;
}
int mid = doc.PageCount / 2;
using (var left = doc.CopyPages(0, mid - 1))
SplitToFiles(left, startPage, targetPages, outDir, files);
using (var right = doc.CopyPages(mid, doc.PageCount - 1))
SplitToFiles(right, startPage + mid, targetPages, outDir, files);
}
// Usage
var chunkFiles = new List<string>();
using (var source = PdfDocument.FromFile("input.pdf"))
SplitToFiles(source, 1, 500, @"C:\temp\chunks", chunkFiles);
The chunk size of 500 is a starting point. Tune it for your documents and hardware.
2. Stamp Each Chunk
Open each chunk, apply the stamp, and save the result to a new file.
var stamper = new HtmlStamper("<div style='font-size:24px;color:red'>CONFIDENTIAL</div>")
{
VerticalAlignment = VerticalAlignment.Middle,
HorizontalAlignment = HorizontalAlignment.Center,
Opacity = 50
};
var stampedFiles = new List<string>();
foreach (string file in chunkFiles)
{
string outPath = file.Replace(".pdf", "_stamped.pdf");
using (var chunk = PdfDocument.FromFile(file))
{
chunk.ApplyStamp(stamper);
chunk.SaveAs(outPath);
}
stampedFiles.Add(outPath);
}
3. Process Chunks in Parallel
Optional: give each worker process its own slice of the chunk list. The benchmark used 8 worker processes on a 16-core machine. Tune the worker count for your hardware.
// In each worker process: workerIndex = 0..workerCount-1
var mySlice = chunkFiles.Where((f, i) => i % workerCount == workerIndex);
4. Merge the Stamped Chunks in Order
var parts = stampedFiles.OrderBy(f => f)
.Select(f => PdfDocument.FromFile(f))
.ToList();
using (var merged = PdfDocument.Merge(parts))
{
merged.SaveAs("output.pdf");
}
parts.ForEach(p => p.Dispose());
Delete the temporary chunk files once you are done.
5. Expected Timings
Results from the 20,000-page benchmark, as a rough guide:
- Splitting: about 6 minutes
- Stamping and merging, single process: about 6 minutes
- Stamping and merging, 8 processes: about 1.5 minutes
- Total parallel run: about 8 minutes
Splitting is the largest cost. If you generate the PDF yourself, consider producing it in chunks from the start and skipping the split.
6. Reuse the Same Pattern for Other Page-Level Operations
Split, process, merge works for any operation that treats each page independently. Good fit:
- Text, image, HTML, and barcode stamps, and watermarks
- HTML headers and footers (set
FirstPageNumberso numbering stays continuous) - Redacting text across all pages
- Find-and-replace text
- Image compression and form flattening
- Backgrounds, foregrounds, and page rotation
Page-dependent stamps such as Bates numbers or page labels need each chunk's global start page. Read it from the chunk file name:
int firstPage = int.Parse(Path.GetFileNameWithoutExtension(file).Split('_')[1]);
for (int i = 0; i < chunk.PageCount; i++)
{
var bates = new TextStamper { Text = $"ABC{firstPage + i:D7}" };
chunk.ApplyStamp(bates, i);
}
The first page number is parsed back out of the chunk_NNNNNN.pdf name, then each page gets a stamp offset from that global start.
Split only, no merge needed:
- Extracting text for search indexing
- Rasterizing pages to images
- Preparing pages for OCR
Run these after merging, not per chunk:
- Digital signatures (merging changes the document and breaks per-chunk signatures)
- Passwords, permissions, and encryption
- Bookmarks, outlines, and tables of contents
- Document metadata
- PDF/A or PDF/UA conversion
Typical use cases:
- Bates numbering for legal discovery sets
- CONFIDENTIAL or DRAFT watermarks on large archives
- Batch statements or invoices with barcodes for mail sorting
- Redacting personal data across exported records
- Shrinking scanned archives before long-term storage
Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.