# How to Control Parallel OCR Memory Usage in C#
Each page processed in parallel by [IronOCR](https://ironsoftware.com/csharp/ocr/) allocates its own native Tesseract engine instance. For large batches - hundreds of pages from a scanned PDF or a folder of images - memory consumption scales linearly with the number of concurrent pages. The [`IronTesseract.MaxDegreeOfParallelism`](https://ironsoftware.com/csharp/ocr/how-to/iron-tesseract/) property caps how many pages are OCR'd at the same time within a single `Read` call, and `IronTesseract.MultiThreaded` switches to fully sequential processing when set to `false`.
*as-heading:2(Quickstart: Cap Parallel OCR with MaxDegreeOfParallelism)*
Set a single property to bound how many pages IronOCR reads at once, keeping peak memory inside a known budget.
```cs
:title=Cap parallel OCR to 4 concurrent pages
var ocr = new IronOcr.IronTesseract { MaxDegreeOfParallelism = 4 };
```
<div class="hsg-featured-snippet">
<h3>Minimal Workflow (5 steps)</h3>
<ol>
<li><a class="js-modal-open" data-modal-id="trial-license-after-download" href="https://nuget.org/packages/IronOcr/">Install IronOCR via NuGet to control OCR parallelism</a></li>
<li>Create an <code>IronTesseract</code> instance</li>
<li>Set <code>MaxDegreeOfParallelism</code> to the desired cap</li>
<li>Load pages into an <code>OcrInput</code></li>
<li>Call <code>Read</code> - only the capped number of pages process concurrently</li>
</ol>
</div>
<br class="clear" />
!!!--LIBRARY_NUGET_INSTALL_BLOCK--!!!
## Why Does OCR Memory Grow During Large Batches?
When `MultiThreaded` is `true`, which is the default, IronOCR processes pages in parallel. Each concurrent page spins up its own native Tesseract engine holding image buffers, recognition models, and intermediate state in unmanaged memory.
Peak memory for a `Read` call therefore has two parts, and only one of them is under your control:
- A **fixed cost for the document itself**, which grows with the number of pages regardless of how they are scheduled.
- A **marginal cost for each page read concurrently**, one native engine apiece.
`MaxDegreeOfParallelism` acts on the second term only. That distinction matters: on a long document the fixed cost can be the larger share, so capping parallelism reduces peak memory substantially without ever driving it to zero.
In containerized environments - Docker with fixed `--memory` limits, Kubernetes pods with resource quotas - the marginal term is what turns a working job into an OOM kill, because it scales with the host's logical processor count rather than with anything you configured. On bare-metal servers it causes swapping and degrades throughput. We cap parallelism to keep the peak working set within a known budget.
## How Much Memory Does Each Concurrent Page Use?
Measured against the reference workload below, those two terms come out as:
**peak ≈ fixed document cost + (per-engine cost × concurrency)**
Take the measured figures as a starting point and measure your own workload to refine them - both terms move with DPI, page complexity, document length, and language pack.
### How Was This Measured?
- Each configuration ran in its own process with the EnglishFast language pack, after a single-page warm-up, five times over.
- Memory is the median of those runs; throughput is the fastest.
Separate processes matter because peak memory is a high-water mark that never resets - a second configuration measured in the same process would re-report the first one's peak. Throughput uses the fastest run rather than the median because other work on the machine can only ever slow a read down, never speed it up.
The corpus is "<a href="/static-assets/ocr/how-to/progress-tracking/Experiences-in-Biodiversity-Research-A-Field-Course.pdf" download="Experiences-in-Biodiversity-Research-A-Field-Course.pdf">Experiences in Biodiversity Research: A Field Course</a>" by Thea B. Gessler, Iowa State University - a 49-page text-dense PDF, downloadable so results can be reproduced. The same runs also confirmed every row of the precedence table further down this page.
### What Were the Results?
| MaxDegreeOfParallelism | Peak memory | Marginal cost per added page | Throughput |
| --- | --- | --- | --- |
| Sequential - `MultiThreaded = false` | 782 MB | baseline | 0.77 pages/s |
| 2 | 859 MB | +77 MB | 1.32 pages/s |
| 4 | 1008 MB | +75 MB | 2.09 pages/s |
| 8 | 1279 MB | +68 MB | 2.56 pages/s |
| 16 - the default on this machine | 1829 MB | +69 MB | 3.01 pages/s |
*Tested on: 8-core / 16-thread x64 laptop CPU · 1.9 GHz · 16 GB RAM · Windows 11 x64 · .NET 8 · IronOCR 2026.8.1 · last verified August 2026*
The first row turns parallelism off altogether; the last is what you get by setting nothing. **Peak memory** is the highest the process reached at any single moment. **Marginal cost per added page** is the extra memory each additional concurrent page cost over the row above it - it stays close to flat down the whole column, and that steadiness is what makes the model worth planning against.
[[i:(Run-to-run variation was under 2% on every configuration. Peak working set is recorded per process, so the memory column is unaffected by other software running at the time; throughput is not, so treat it as specific to this hardware.)]]
### Where Do Diminishing Returns Start?
Memory grows steadily with concurrency. Throughput does not - the gains flatten out while the memory cost keeps climbing at the same rate. Compare the last two rows: the memory column rises far more sharply between them than the throughput column does.
That is the practical finding: **the default is rarely the best trade for batch work.** It is tuned for latency on a single document, not for footprint. If you are processing documents in bulk on a memory-constrained host, an explicit cap somewhere below the logical processor count will usually cost you very little speed and save a great deal of memory. Measure where the knee falls on your own documents, because it moves with page complexity.
## How Do I Cap How Many Pages Are Read at Once?
Set `IronTesseract.MaxDegreeOfParallelism` to the maximum number of pages that should process concurrently within a single `Read` call. Left alone, it defaults to `Environment.ProcessorCount`.
```csharp
using IronOcr;
using System;
var ocr = new IronTesseract();
// Cap concurrency at 2 pages. Each concurrent page holds its own native
// engine, so this bounds peak memory for every Read on this instance.
ocr.MaxDegreeOfParallelism = 2;
// The cap applies per Read call, however many pages the document has.
using var input = new OcrInput();
input.LoadPdf("500-page-archive.pdf");
OcrResult result = ocr.Read(input);
Console.WriteLine(result.Text.Length);
```
### What Value Should I Choose?
Size it against the two-part model above rather than picking a number:
**fixed document cost** + **per-engine cost** × **MaxDegreeOfParallelism** ≤ **memory budget for OCR**
To fill in the two costs for your own documents, read a representative file twice - once with `MultiThreaded` set to `false`, once with `MaxDegreeOfParallelism = 2` - and record the peak working set each time. The sequential run gives you the fixed cost; the difference between the two gives you the cost of one extra engine. The reference figures for both are in the table above.
Two things worth noting when you apply it. The fixed term scales with document length, so a long archive will not behave like the sample measured above. And leave headroom for the .NET runtime, the managed heap, and anything else in the process - reserving around 40% of the container limit is a reasonable starting point.
Memory-generous hosts can leave the default in place. Constrained ones should cap explicitly, and as the diminishing-returns figures above show, that cap often costs far less throughput than its memory saving suggests.
[[w:(`MaxDegreeOfParallelism` bounds pages within one `Read` call only. If your own code also runs several documents at once, the two multiply - four concurrent documents against a default of 16 is 64 engines, not 16. Cap both layers. See [High Peak Memory During Bulk OCR](https://ironsoftware.com/csharp/ocr/troubleshooting/bulk-ocr-peak-memory/) for the outer-loop pattern.)]]
## How Do I Read Pages Sequentially?
Set `MultiThreaded` to `false`. This is the lowest-memory option, because only one native engine is ever active. The trade-off is throughput: a batch takes roughly N times longer than with N concurrent workers.
```csharp
using IronOcr;
var ocr = new IronTesseract();
// One page at a time. Overrides MaxDegreeOfParallelism, whatever it is set to.
ocr.MultiThreaded = false;
using var input = new OcrInput();
input.LoadPdf("scans.pdf");
OcrResult result = ocr.Read(input);
```
[[w:(`MultiThreaded` had no effect before this release - it is now a live setting. Code that set it to `false` expecting a no-op will read sequentially after upgrading, with no compiler warning and no exception. Review existing usages if throughput drops unexpectedly.)]]
## How Do the Two Properties Interact?
| MultiThreaded | MaxDegreeOfParallelism | Effective concurrency |
| --- | --- | --- |
| `true` (default) | Not set | `Environment.ProcessorCount` |
| `true` | A positive number | That number of concurrent pages |
| `true` | `0` or negative | Falls back to `Environment.ProcessorCount` |
| `false` | Any value | 1 - sequential, `MaxDegreeOfParallelism` is ignored |
[[t:(`Environment.ProcessorCount` reports **logical processors**, not physical cores. On a machine with simultaneous multithreading the default is twice the core count - an 8-core CPU defaults to 16 concurrent pages, not 8. Size your memory budget against the logical count.)]]
## Can I Bound Memory During Page Orientation Detection?
The new overload of [`OcrInput.DetectPageOrientation`](https://ironsoftware.com/csharp/ocr/how-to/detect-page-rotation/) accepts a `maxDegreeOfParallelism` parameter that caps how many pages are orientation-detected concurrently. Non-positive values fall back to `Environment.ProcessorCount`. The single-argument overload is unchanged and remains source and binary compatible.
```csharp
using IronOcr;
using System;
using var input = new OcrInput();
input.LoadPdf("rotated-scans.pdf");
// Analyse at most 2 pages at once. Orientation detection runs before the
// OCR read, so capping it here prevents a spike ahead of the real workload.
var results = input.DetectPageOrientation(
OrientationDetectionMode.Fast,
maxDegreeOfParallelism: 2);
// One result per page, in page order.
foreach (var result in results)
{
Console.WriteLine($"Page {result.PageNumber}: {result.RotationAngle}");
}
```
`Fast` mode reuses one engine per worker thread instead of allocating one per page, which reduces peak memory further. This matters when orientation detection runs as a preprocessing step before the main OCR read, since capping parallelism here prevents a memory spike before the real workload begins.
## Does Parallel OCR Still Crash on Large Batches?
Previous versions could throw a fatal `AccessViolationException` during parallel or high-volume OCR due to unsafe cross-thread access to native Tesseract state. That exception is unrecoverable - it terminates the process without a catch opportunity. It is now resolved. The previous workarounds, one `IronTesseract` instance per thread and one document at a time, are no longer necessary: the engine isolates native state per worker correctly, and `MaxDegreeOfParallelism` provides an official cap that replaces those ad-hoc patterns.
## When Should I Tune These Settings?
- **Container or CI runner with a memory cap** - set `MaxDegreeOfParallelism` to keep peak memory within the limit.
- **Batch processing thousands of pages** - lower `MaxDegreeOfParallelism` to prevent OOM kills. Throughput decreases proportionally, but the job completes instead of crashing.
- **Lowest possible memory** - set `MultiThreaded` to `false` for sequential mode. One engine, one page at a time.
- **Maximum throughput** - leave both at their defaults so the engine uses all available cores. See the [multithreading for speed example](https://ironsoftware.com/csharp/ocr/examples/csharp-tesseract-multithreading-for-speed/).
- **Accuracy versus speed tuning** - that is a different axis. The [tune Tesseract for speed guide](https://ironsoftware.com/csharp/ocr/examples/tune-tesseract-for-speed-in-dotnet/) covers accuracy and speed trade-offs, while this page covers memory and throughput trade-offs.
For async and responsiveness concerns, such as keeping a UI responsive during OCR, see the [async OCR how-to](https://ironsoftware.com/csharp/ocr/how-to/async/), which owns `ReadAsync` and `OcrReadTask`. This page covers resource bounding: keeping memory consumption predictable regardless of batch size.
Explore the [IronTesseract configuration guide](https://ironsoftware.com/csharp/ocr/how-to/iron-tesseract/) for the full property surface, [loading PDF inputs](https://ironsoftware.com/csharp/ocr/how-to/input-pdfs/) and [loading image inputs](https://ironsoftware.com/csharp/ocr/how-to/input-images/) for `OcrInput` usage patterns, and [page orientation detection](https://ironsoftware.com/csharp/ocr/how-to/detect-page-rotation/) for the `DetectPageOrientation` method and its new parallelism overload.
Explore [IronOCR licensing options](https://ironsoftware.com/csharp/ocr/licensing/) to move from trial to production.
Each page processed in parallel by IronOCR allocates its own native Tesseract engine instance. For large batches - hundreds of pages from a scanned PDF or a folder of images - memory consumption scales linearly with the number of concurrent pages. The IronTesseract.MaxDegreeOfParallelism property caps how many pages are OCR'd at the same time within a single Read call, and IronTesseract.MultiThreaded switches to fully sequential processing when set to false.
Quickstart: Cap Parallel OCR with MaxDegreeOfParallelism
Set a single property to bound how many pages IronOCR reads at once, keeping peak memory inside a known budget.
1Install IronOCR with NuGet Package Manager
PM > Install-Package IronOcr
Install-Package IronOcr
2Copy and run this code snippet.
var ocr = new IronOcr.IronTesseract { MaxDegreeOfParallelism = 4 };
var ocr = new IronOcr.IronTesseract { MaxDegreeOfParallelism = 4 };
C#
3Deploy to test on your live environment
Start using IronOCR in your project today with a free trial
Call Read - only the capped number of pages process concurrently
Install with NuGet
PM > Install-Package IronOcr
Install-Package IronOcr
Install IronOCR by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.
Why Does OCR Memory Grow During Large Batches?
When MultiThreaded is true, which is the default, IronOCR processes pages in parallel. Each concurrent page spins up its own native Tesseract engine holding image buffers, recognition models, and intermediate state in unmanaged memory.
Peak memory for a Read call therefore has two parts, and only one of them is under your control:
A fixed cost for the document itself, which grows with the number of pages regardless of how they are scheduled.
A marginal cost for each page read concurrently, one native engine apiece.
MaxDegreeOfParallelism acts on the second term only. That distinction matters: on a long document the fixed cost can be the larger share, so capping parallelism reduces peak memory substantially without ever driving it to zero.
In containerized environments - Docker with fixed --memory limits, Kubernetes pods with resource quotas - the marginal term is what turns a working job into an OOM kill, because it scales with the host's logical processor count rather than with anything you configured. On bare-metal servers it causes swapping and degrades throughput. We cap parallelism to keep the peak working set within a known budget.
How Much Memory Does Each Concurrent Page Use?
Measured against the reference workload below, those two terms come out as:
Take the measured figures as a starting point and measure your own workload to refine them - both terms move with DPI, page complexity, document length, and language pack.
How Was This Measured?
Each configuration ran in its own process with the EnglishFast language pack, after a single-page warm-up, five times over.
Memory is the median of those runs; throughput is the fastest.
Separate processes matter because peak memory is a high-water mark that never resets - a second configuration measured in the same process would re-report the first one's peak. Throughput uses the fastest run rather than the median because other work on the machine can only ever slow a read down, never speed it up.
The corpus is "Experiences in Biodiversity Research: A Field Course" by Thea B. Gessler, Iowa State University - a 49-page text-dense PDF, downloadable so results can be reproduced. The same runs also confirmed every row of the precedence table further down this page.
What Were the Results?
MaxDegreeOfParallelism
Peak memory
Marginal cost per added page
Throughput
Sequential - MultiThreaded = false
782 MB
baseline
0.77 pages/s
2
859 MB
+77 MB
1.32 pages/s
4
1008 MB
+75 MB
2.09 pages/s
8
1279 MB
+68 MB
2.56 pages/s
16 - the default on this machine
1829 MB
+69 MB
3.01 pages/s
Tested on: 8-core / 16-thread x64 laptop CPU · 1.9 GHz · 16 GB RAM · Windows 11 x64 · .NET 8 · IronOCR 2026.8.1 · last verified August 2026
The first row turns parallelism off altogether; the last is what you get by setting nothing. Peak memory is the highest the process reached at any single moment. Marginal cost per added page is the extra memory each additional concurrent page cost over the row above it - it stays close to flat down the whole column, and that steadiness is what makes the model worth planning against.
Please note: Run-to-run variation was under 2% on every configuration. Peak working set is recorded per process, so the memory column is unaffected by other software running at the time; throughput is not, so treat it as specific to this hardware.
Where Do Diminishing Returns Start?
Memory grows steadily with concurrency. Throughput does not - the gains flatten out while the memory cost keeps climbing at the same rate. Compare the last two rows: the memory column rises far more sharply between them than the throughput column does.
That is the practical finding: the default is rarely the best trade for batch work. It is tuned for latency on a single document, not for footprint. If you are processing documents in bulk on a memory-constrained host, an explicit cap somewhere below the logical processor count will usually cost you very little speed and save a great deal of memory. Measure where the knee falls on your own documents, because it moves with page complexity.
How Do I Cap How Many Pages Are Read at Once?
Set IronTesseract.MaxDegreeOfParallelism to the maximum number of pages that should process concurrently within a single Read call. Left alone, it defaults to Environment.ProcessorCount.
using IronOcr;using System;var ocr = new IronTesseract();// Cap concurrency at 2 pages. Each concurrent page holds its own native// engine, so this bounds peak memory for every Read on this instance.ocr.MaxDegreeOfParallelism = 2;// The cap applies per Read call, however many pages the document has.using var input = new OcrInput();input.LoadPdf("500-page-archive.pdf");OcrResult result = ocr.Read(input);Console.WriteLine(result.Text.Length);
using IronOcr;
using System;
var ocr = new IronTesseract();
// Cap concurrency at 2 pages. Each concurrent page holds its own native
// engine, so this bounds peak memory for every Read on this instance.
ocr.MaxDegreeOfParallelism = 2;
// The cap applies per Read call, however many pages the document has.
using var input = new OcrInput();
input.LoadPdf("500-page-archive.pdf");
OcrResult result = ocr.Read(input);
Console.WriteLine(result.Text.Length);
C#
What Value Should I Choose?
Size it against the two-part model above rather than picking a number:
To fill in the two costs for your own documents, read a representative file twice - once with MultiThreaded set to false, once with MaxDegreeOfParallelism = 2 - and record the peak working set each time. The sequential run gives you the fixed cost; the difference between the two gives you the cost of one extra engine. The reference figures for both are in the table above.
Two things worth noting when you apply it. The fixed term scales with document length, so a long archive will not behave like the sample measured above. And leave headroom for the .NET runtime, the managed heap, and anything else in the process - reserving around 40% of the container limit is a reasonable starting point.
Memory-generous hosts can leave the default in place. Constrained ones should cap explicitly, and as the diminishing-returns figures above show, that cap often costs far less throughput than its memory saving suggests.
Warning: MaxDegreeOfParallelism bounds pages within one Read call only. If your own code also runs several documents at once, the two multiply - four concurrent documents against a default of 16 is 64 engines, not 16. Cap both layers. See High Peak Memory During Bulk OCR for the outer-loop pattern.
How Do I Read Pages Sequentially?
Set MultiThreaded to false. This is the lowest-memory option, because only one native engine is ever active. The trade-off is throughput: a batch takes roughly N times longer than with N concurrent workers.
using IronOcr;var ocr = new IronTesseract();// One page at a time. Overrides MaxDegreeOfParallelism, whatever it is set to.ocr.MultiThreaded = false;using var input = new OcrInput();input.LoadPdf("scans.pdf");OcrResult result = ocr.Read(input);
using IronOcr;
var ocr = new IronTesseract();
// One page at a time. Overrides MaxDegreeOfParallelism, whatever it is set to.
ocr.MultiThreaded = false;
using var input = new OcrInput();
input.LoadPdf("scans.pdf");
OcrResult result = ocr.Read(input);
C#
Warning: MultiThreaded had no effect before this release - it is now a live setting. Code that set it to false expecting a no-op will read sequentially after upgrading, with no compiler warning and no exception. Review existing usages if throughput drops unexpectedly.
How Do the Two Properties Interact?
MultiThreaded
MaxDegreeOfParallelism
Effective concurrency
true (default)
Not set
Environment.ProcessorCount
true
A positive number
That number of concurrent pages
true
0 or negative
Falls back to Environment.ProcessorCount
false
Any value
1 - sequential, MaxDegreeOfParallelism is ignored
Tips: Environment.ProcessorCount reports logical processors, not physical cores. On a machine with simultaneous multithreading the default is twice the core count - an 8-core CPU defaults to 16 concurrent pages, not 8. Size your memory budget against the logical count.
Can I Bound Memory During Page Orientation Detection?
The new overload of OcrInput.DetectPageOrientation accepts a maxDegreeOfParallelism parameter that caps how many pages are orientation-detected concurrently. Non-positive values fall back to Environment.ProcessorCount. The single-argument overload is unchanged and remains source and binary compatible.
using IronOcr;using System;using var input = new OcrInput();input.LoadPdf("rotated-scans.pdf");// Analyse at most 2 pages at once. Orientation detection runs before the// OCR read, so capping it here prevents a spike ahead of the real workload.var results = input.DetectPageOrientation(OrientationDetectionMode.Fast, maxDegreeOfParallelism: 2);// One result per page, in page order.foreach (var result in results){Console.WriteLine($"Page {result.PageNumber}: {result.RotationAngle}");}
using IronOcr;
using System;
using var input = new OcrInput();
input.LoadPdf("rotated-scans.pdf");
// Analyse at most 2 pages at once. Orientation detection runs before the
// OCR read, so capping it here prevents a spike ahead of the real workload.
var results = input.DetectPageOrientation(
OrientationDetectionMode.Fast,
maxDegreeOfParallelism: 2);
// One result per page, in page order.
foreach (var result in results)
{
Console.WriteLine($"Page {result.PageNumber}: {result.RotationAngle}");
}
C#
Fast mode reuses one engine per worker thread instead of allocating one per page, which reduces peak memory further. This matters when orientation detection runs as a preprocessing step before the main OCR read, since capping parallelism here prevents a memory spike before the real workload begins.
Does Parallel OCR Still Crash on Large Batches?
Previous versions could throw a fatal AccessViolationException during parallel or high-volume OCR due to unsafe cross-thread access to native Tesseract state. That exception is unrecoverable - it terminates the process without a catch opportunity. It is now resolved. The previous workarounds, one IronTesseract instance per thread and one document at a time, are no longer necessary: the engine isolates native state per worker correctly, and MaxDegreeOfParallelism provides an official cap that replaces those ad-hoc patterns.
When Should I Tune These Settings?
Container or CI runner with a memory cap - set MaxDegreeOfParallelism to keep peak memory within the limit.
Batch processing thousands of pages - lower MaxDegreeOfParallelism to prevent OOM kills. Throughput decreases proportionally, but the job completes instead of crashing.
Lowest possible memory - set MultiThreaded to false for sequential mode. One engine, one page at a time.
Maximum throughput - leave both at their defaults so the engine uses all available cores. See the multithreading for speed example.
Accuracy versus speed tuning - that is a different axis. The tune Tesseract for speed guide covers accuracy and speed trade-offs, while this page covers memory and throughput trade-offs.
For async and responsiveness concerns, such as keeping a UI responsive during OCR, see the async OCR how-to, which owns ReadAsync and OcrReadTask. This page covers resource bounding: keeping memory consumption predictable regardless of batch size.
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.