# How to Convert Multiple PDFs with olmOCR: Batch Processing Guide

> Convert multiple PDFs efficiently using olmOCR. Learn how to leverage its batch processing capabilities with the WorkQueue class for streamlined OCR tasks.

- Repository: [Ai2/olmocr](https://github.com/allenai/olmocr)
- Tags: how-to-guide
- Published: 2026-07-02

---

**Use the `WorkQueue` class with `LocalBackend` or `S3Backend` to distribute per-page OCR jobs across async workers, calling `process_page` from `olmocr.pipeline` for each page.**

olmOCR (allenai/olmocr) is an open-source OCR pipeline that converts PDF pages to markdown using vision language models. To **convert multiple PDFs with olmOCR** efficiently, you leverage its async `WorkQueue` infrastructure that treats every PDF page as an independent work unit, enabling parallel processing across local files or cloud storage.

## Understanding the olmOCR Batch Architecture

olmOCR employs a **pipeline-driven architecture** where PDF conversion is split into discrete, asynchronous operations. The system relies on three core components working together:

- **`olmocr.pipeline`** – Implements the end-to-end OCR flow in [`olmocr/pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/pipeline.py). The public entry point is `process_page`, which renders a PDF page to an image, builds a VLLM request, and parses the response into a `PageResult` object.
- **`olmocr.work_queue.WorkQueue`** – Manages a queue of jobs and distributes them to a pool of workers. It supports `LocalBackend` for filesystem operations or `S3Backend` for cloud storage.
- **[`olmocr/bench/runners/run_olmocr_pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/bench/runners/run_olmocr_pipeline.py)** – Demonstrates the minimal async wrapper pattern (lines 42-84) for single-page execution, which scales to batch operations when integrated with the work queue.

### The Data Flow for Bulk Conversion

1. **Discover PDFs** – Collect absolute paths using standard file walkers or dataset utilities like [`prepare_olmocrmix.py`](https://github.com/allenai/olmocr/blob/main/prepare_olmocrmix.py).
2. **Create a `WorkQueue`** – Instantiate with `LocalBackend` (local disk) or `S3Backend` (S3 storage) and configure worker count.
3. **Enqueue jobs** – Add PDF paths; the queue automatically normalizes each into per-page tasks.
4. **Process pages** – Workers invoke `process_page(args, worker_id, pdf_orig_path, pdf_local_path, page_num)` for each page.
5. **Collect results** – Retrieve `PageResult` objects containing extracted markdown text.
6. **Shutdown** – Workers exit cleanly when the queue empties; the VLLM server remains available for reuse.

## Converting Local PDFs in Batch

For processing directories of PDFs on your local machine, combine the `WorkQueue` with `LocalBackend` and ensure the VLLM server is running before enqueuing jobs.

### Step 1: Configure Pipeline Arguments

Create a configuration object matching the expected arguments for `process_page`:

```python
import asyncio
import os
from pathlib import Path
from olmocr.pipeline import MetricsKeeper, WorkerTracker, process_page, vllm_server_ready, vllm_server_host
from olmocr.work_queue import WorkQueue, LocalBackend

class Args:
    model = "allenai/olmOCR-2-7B-1025-FP8"
    server = "http://localhost:30044/v1"
    port = 30044
    model_chat_template = "qwen2-vl"
    max_model_len = 16384
    guided_decoding = False
    gpu_memory_utilization = 0.8
    target_longest_image_dim = 1288
    target_anchor_text_len = -1
    max_page_retries = 8
    max_page_error_rate = 0.004
    tensor_parallel_size = 1
    data_parallel_size = 1

args = Args()

```

### Step 2: Ensure VLLM Server Availability

Before processing, verify or start the VLLM server using patterns from [`run_olmocr_pipeline.py`](https://github.com/allenai/olmocr/blob/main/run_olmocr_pipeline.py):

```python
async def ensure_server():
    """Start a VLLM server if one is not already running."""
    try:
        await asyncio.wait_for(vllm_server_ready(args), timeout=5)
    except Exception:
        sem = asyncio.Semaphore(1)
        asyncio.create_task(vllm_server_host(args.model, args, sem))
        await vllm_server_ready(args)

```

### Step 3: Process Files with WorkQueue

Initialize the queue, enqueue all PDFs, and run the conversion:

```python
async def convert_pdf_directory(pdf_dir: str):
    await ensure_server()
    
    # Initialize backend and queue with CPU count workers

    backend = LocalBackend()
    queue = WorkQueue(backend=backend, max_workers=os.cpu_count())
    
    # Enqueue every PDF found in directory

    pdf_paths = list(Path(pdf_dir).rglob("*.pdf"))
    for pdf_path in pdf_paths:
        await queue.enqueue_job(str(pdf_path))
    
    # Execute queue - workers call process_page internally for each page

    await queue.run()
    print(f"✅ Converted {len(pdf_paths)} PDFs")

if __name__ == "__main__":
    asyncio.run(convert_pdf_directory("/path/to/pdfs"))

```

Under the hood, `WorkQueue` reads each PDF to determine page count, creates a task per page, and distributes them across workers respecting concurrency limits.

## Processing PDFs from S3

When source files reside in cloud storage, swap `LocalBackend` for `S3Backend` without changing the processing logic:

```python
from olmocr.work_queue import WorkQueue, S3Backend

# Configure S3 backend with your boto3 client

backend = S3Backend(
    s3_client=boto3.client('s3'),
    bucket="my-pdf-bucket",
    prefix="documents/",
    output_bucket="my-output-bucket"  # Optional: for writing results back to S3

)

queue = WorkQueue(backend=backend, max_workers=4)
await queue.run()

```

The `S3Backend` automatically downloads PDFs to temporary local storage for processing and can upload markdown results back to the specified output bucket.

## Managing Concurrency and Resource Limits

Concurrency is controlled by two key semaphores defined in [`olmocr/pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/pipeline.py) (lines 87-90):

- **`max_concurrent_requests_limit`** – Defaults to 1, limiting simultaneous VLLM API calls.
- **`pdf_render_max_workers_limit`** – Derived from CPU count, limiting parallel PDF rendering operations.

The `WorkQueue` respects these limits automatically, ensuring you do not overwhelm the VLLM server or exhaust system memory when converting hundreds of PDFs simultaneously.

## Summary

- **Use `WorkQueue`** with `LocalBackend` or `S3Backend` to orchestrate batch conversions without managing threads manually.
- **Call `process_page`** for each page; it handles rendering, VLLM communication, and retry logic defined in [`olmocr/pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/pipeline.py).
- **Manage the VLLM server lifecycle** using `vllm_server_ready` and `vllm_server_host` to ensure the model is available before queuing jobs.
- **Leverage async architecture** to process dozens or hundreds of PDFs concurrently, limited only by GPU memory and the `max_concurrent_requests_limit` semaphore.

## Frequently Asked Questions

### How does olmOCR handle concurrency when converting multiple PDFs?

olmOCR uses an **asyncio-based work queue** that treats each PDF page as an independent task. The `WorkQueue` class spins up multiple worker coroutines (configurable via `max_workers`) that consume tasks concurrently. Actual concurrency is bounded by semaphores in [`olmocr/pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/pipeline.py) that limit simultaneous VLLM requests and PDF rendering operations to prevent resource exhaustion.

### Can I process PDFs stored in S3 with olmOCR?

Yes. Replace `LocalBackend` with `S3Backend` from `olmocr.work_queue`, passing a boto3 client and bucket configuration. The backend handles downloading PDFs to temporary local storage before processing and supports writing markdown outputs directly back to S3 via the `output_bucket` parameter.

### What is the recommended way to handle VLLM server startup for large batches?

Follow the pattern in [`olmocr/bench/runners/run_olmocr_pipeline.py`](https://github.com/allenai/olmocr/blob/main/olmocr/bench/runners/run_olmocr_pipeline.py): use `vllm_server_ready()` to check for an existing server, and if none responds within your timeout, launch `vllm_server_host()` as a background task. This ensures the model loads once and remains available for all subsequent PDFs in your batch, avoiding the overhead of repeated model initialization.

### How do I retrieve the extracted markdown text from the pipeline results?

The `process_page` function returns a `PageResult` object. Access the extracted text via `result.response.natural_text` after verifying that `result` exists and `result.is_fallback` is False (indicating successful OCR rather than error fallback). When using `WorkQueue`, attach a callback to the queue or modify the result handler to write outputs to your preferred storage format.