Summary
The default PDF parser holds the entire document in memory, and on one recovery path holds two full copies at once. The Indexer actors that run it have no restart policy, so a worker that OOMs is not recovered the way Marker/Docling workers are.
This is the memory-residency half of a third-party audit finding. The admission control half is already tracked in #663 — this issue is deliberately scoped to per-file residency and worker recovery.
1. The default PDF parser is fully resident
openrag/core/indexing/parsers/pdf/pymupdf.py opens from bytes, not a path:
def _extract_markdown(raw: bytes, filename: str):
with pymupdf.open(stream=raw, filetype="pdf") as doc: # whole file resident
try:
chunks = _to_markdown(doc)
except RuntimeError:
cleaned = doc.tobytes(garbage=4, clean=True) # second full copy
with pymupdf.open(stream=cleaned, filetype="pdf") as clean_doc:
chunks = _to_markdown(clean_doc)
raw and cleaned are both live inside the except block, so peak usage is roughly double — on the malformed-document recovery path added in #640, which is exactly where unusual (often large) documents end up.
pymupdf4llm.to_markdown(doc, page_chunks=True, ...) chunks the output per page; the document is already fully loaded by then.
This is the shipped default in all three places: conf/config.yaml:202 (pdf: PyMuPDFLoader), infra/compose/.env.example:46, and infra/charts/openrag-stack/values.yaml:666. So a default chart or compose deployment is on this path.
Marker is not affected — it splits into page ranges (marker_workers.py:293-298) and passes a path plus page_range to each worker (marker_workers.py:176-191), so its memory is bounded per batch. Anyone who has switched PDFLOADER=MarkerLoader is not exposed here.
2. Indexer actors have no restart policy
indexer_pool.py:41 and :355 declare bare @ray.remote, with no max_restarts. Marker and Docling deliberately set it:
marker_workers.py:82 and docling_workers.py:85,97 → @ray.remote(max_restarts=5), with comments explaining that a worker which OOMs or CUDA-faults must come back.
So the tier most likely to OOM on a large PDF is the one with no recovery configured, while the GPU tiers have it.
3. .doc conversion reads the converted file whole
core/indexing/parsers/doc_parser.py:87 — return Path(out_path).read_bytes(), None after the Spire.Doc .doc → .docx conversion. Smaller in practice, but same class of issue.
Suggested direction
Found by
An independent third-party audit of OpenRag; verified against v2.1.0. The audit stated this more broadly ("the indexing worker loads the complete file into memory"); narrowed here to what reproduces — most parsers do work from a path, and _sha256_file correctly streams in 1 MB chunks.
Summary
The default PDF parser holds the entire document in memory, and on one recovery path holds two full copies at once. The Indexer actors that run it have no restart policy, so a worker that OOMs is not recovered the way Marker/Docling workers are.
This is the memory-residency half of a third-party audit finding. The admission control half is already tracked in #663 — this issue is deliberately scoped to per-file residency and worker recovery.
1. The default PDF parser is fully resident
openrag/core/indexing/parsers/pdf/pymupdf.pyopens from bytes, not a path:rawandcleanedare both live inside theexceptblock, so peak usage is roughly double — on the malformed-document recovery path added in #640, which is exactly where unusual (often large) documents end up.pymupdf4llm.to_markdown(doc, page_chunks=True, ...)chunks the output per page; the document is already fully loaded by then.This is the shipped default in all three places:
conf/config.yaml:202(pdf: PyMuPDFLoader),infra/compose/.env.example:46, andinfra/charts/openrag-stack/values.yaml:666. So a default chart or compose deployment is on this path.Marker is not affected — it splits into page ranges (
marker_workers.py:293-298) and passes a path pluspage_rangeto each worker (marker_workers.py:176-191), so its memory is bounded per batch. Anyone who has switchedPDFLOADER=MarkerLoaderis not exposed here.2. Indexer actors have no restart policy
indexer_pool.py:41and:355declare bare@ray.remote, with nomax_restarts. Marker and Docling deliberately set it:marker_workers.py:82anddocling_workers.py:85,97→@ray.remote(max_restarts=5), with comments explaining that a worker which OOMs or CUDA-faults must come back.So the tier most likely to OOM on a large PDF is the one with no recovery configured, while the GPU tiers have it.
3.
.docconversion reads the converted file wholecore/indexing/parsers/doc_parser.py:87—return Path(out_path).read_bytes(), Noneafter the Spire.Doc.doc→.docxconversion. Smaller in practice, but same class of issue.Suggested direction
rawandcleanedsimultaneously in the One unparseable page fails the whole document: PDF parse stage has no fallback when PyMuPDF raises (code=4: no font file for digest) #640 recovery path.max_restartsconsistent with the Marker/Docling pools.Found by
An independent third-party audit of OpenRag; verified against
v2.1.0. The audit stated this more broadly ("the indexing worker loads the complete file into memory"); narrowed here to what reproduces — most parsers do work from a path, and_sha256_filecorrectly streams in 1 MB chunks.