How We Reduced Document Extraction Cost by 5x with Tesseract Pre-processing
How We Reduced Document Extraction Cost by 5x with Tesseract Pre-processing
Update (June 2026): This post describes our Claude-only extraction pipeline as it stood in April 2026. Since 18 May 2026, ExtractBee runs Google Gemini 2.5 Flash as the primary extraction model, with Claude Sonnet 4.6 as an automatic fallback, which cut per-page model cost further. The Tesseract OCR pre-processing, confidence-based fallback, caching, and dedup techniques below are still in production; only the primary model changed.
When we launched ExtractBee's first extraction pipeline, we were sending raw document images directly to Claude's Vision API. It worked well — Claude reads images natively and extraction quality was good. But the cost was higher than we expected.
Our first real-world measurement: $0.026 per page. At that rate, the economics of our pricing model were uncomfortable at higher volumes.
This post covers how we brought that cost down to $0.005–0.013 per page by adding Tesseract OCR pre-processing — without any reduction in extraction quality.
Why the Initial Approach Was Expensive
Claude's Vision API processes images by converting them to tokens internally. A single page of an invoice as an image might consume 800–1,500 input tokens just for the image representation — before Claude has even started reading the content.
The key insight: Claude is extraordinarily good at reading text. It's a language model — text is its native domain. (For the reasoning behind why we originally chose Claude as our extraction model, and how the wider pipeline fits together, see our invoice extraction architecture deep-dive.) If we give Claude the text content of a document rather than an image of that document, it can process the same information using far fewer tokens, at the lower text token price rather than the Vision price.
The problem is that we receive documents as images (scanned PDFs, photographs) — not as text. The solution is OCR: convert the image to text first, then give the text to Claude.
Before: Image → Claude Vision
Scanned invoice (image PDF)
↓
[No pre-processing]
↓
Claude Vision API
(image tokens: ~1,200 per page)
(output tokens: ~400 per page)
↓
Cost per page: ~$0.026
Breakdown at $0.026/page:
- Input (image tokens): ~$0.018
- Output (text tokens): ~$0.008
After: Image → Tesseract → Claude Text
Scanned invoice (image PDF)
↓
pdf2pic: PDF → PNG at 300 DPI
↓
Tesseract.js: image → text
↓
Text cleaning: remove noise
↓
Claude Text API
(input tokens: ~800 per page)
(output tokens: ~350 per page)
↓
Cost per page: ~$0.006–0.013
Breakdown at $0.013/page (without prompt caching):
- Input (text tokens): ~$0.006
- Output (text tokens): ~$0.007
With prompt caching active (subsequent calls):
- Input (cached system prompt): ~$0.001
- Input (document text): ~$0.004
- Output: ~$0.005
- Total: ~$0.010/page
The Implementation
We use Tesseract.js — the JavaScript port of Tesseract OCR — which runs entirely in Node.js without binary dependencies.
import Tesseract from 'tesseract.js';
import { fromBuffer } from 'pdf2pic';
interface OcrResult {
text: string;
confidence: number; // 0-100
usedOcr: boolean;
}
async function extractTextFromDocument(
fileBuffer: Buffer,
mimeType: string
): Promise<OcrResult> {
// Digital PDFs: extract text layer directly
if (mimeType === 'application/pdf') {
const directText = await extractPdfTextLayer(fileBuffer);
if (directText.length > 100) {
// Text layer exists and has content
return {
text: cleanText(directText),
confidence: 100,
usedOcr: false
};
}
// Fall through to OCR if text layer is empty (image-based PDF)
}
// Image-based PDFs and image files: run OCR
const images = await convertToImages(fileBuffer, mimeType);
const pages: string[] = [];
let totalConfidence = 0;
for (const imageBuffer of images) {
const result = await Tesseract.recognize(imageBuffer, 'eng+deu+fra+lit+lav+pol+spa+ita', {
logger: () => {} // suppress progress logs
});
pages.push(result.data.text);
totalConfidence += result.data.confidence;
}
const rawText = pages.join('\n\n--- PAGE BREAK ---\n\n');
const avgConfidence = totalConfidence / images.length;
return {
text: cleanText(rawText),
confidence: avgConfidence,
usedOcr: true
};
}
function cleanText(raw: string): string {
return raw
.replace(/^\s*[\r\n]/gm, '') // remove empty lines
.replace(/[ \t]+/g, ' ') // collapse whitespace
.replace(/[^\w\s€$£.,:\-\/\(\)@#%]/g, '') // remove noise chars
.trim();
}
Smart fallback based on OCR confidence:
async function processDocument(fileBuffer: Buffer, mimeType: string) {
const { text, confidence, usedOcr } = await extractTextFromDocument(
fileBuffer,
mimeType
);
if (usedOcr && confidence < 70) {
// OCR quality too low — fall back to Claude Vision
// Log for monitoring: this document may need manual review
return await extractWithClaudeVision(fileBuffer);
}
// Good quality text — use Claude Text API (cheaper)
return await extractWithClaudeText(text);
}
This means:
- Digital PDFs → always Claude Text (fast, cheap, accurate)
- Scanned PDFs with good OCR confidence (≥70%) → Claude Text
- Scanned PDFs with poor OCR confidence (<70%) → Claude Vision fallback
In our production data, about 85% of scanned documents have OCR confidence above 70% — so the Vision fallback is rare.
What We Measured
We ran both approaches on the same set of 500 test documents (a mix of digital PDFs and scanned invoices from various vendors) and compared cost and extraction quality.
Cost comparison:
| Approach | Avg cost/page | Cost per 1,000 pages |
|---|---|---|
| Claude Vision (baseline) | $0.026 | $26.00 |
| Tesseract + Claude Text | $0.013 | $13.00 |
| + Prompt caching | $0.010 | $10.00 |
| + Text cleaning | $0.008 | $8.00 |
| + File hash dedup (dupes removed) | ~$0.006 effective | ~$6.00 |
Extraction quality:
Quality remained equivalent or better on digital PDFs and good-quality scans. On poor-quality scans (low DPI, skewed, overexposed), OCR sometimes produces garbled text that confuses Claude — which is exactly why we implemented the confidence-based fallback to Vision for those cases.
Additional Cost Optimisations
Tesseract pre-processing was the biggest single win, but we implemented several other optimisations alongside it.
Prompt caching (cache_control: ephemeral)
The system prompt for invoice extraction is ~1,000 tokens and identical for every invoice extraction. Anthropic's prompt caching charges ~10% of the normal input token price for cached tokens after the first call (Anthropic pricing as of April 2026; verify current figures against their docs). This saves approximately 25% on input costs for high-frequency document types.
Minified JSON output We instruct Claude to return minified JSON — no whitespace, no explanation, no markdown. This reduces output token count by 15–20% compared to prettified JSON.
File hash deduplication We compute SHA-256 of each uploaded file before extraction. If we've extracted the same file within 7 days (Redis TTL), we return the cached result at $0 additional cost. In practice, duplicate submissions account for 3–8% of all uploads — not enormous, but entirely free to handle.
Temperature: 0 No impact on cost, but significant impact on consistency. Extraction results are deterministic — the same document always produces the same output.
What We'd Still Like to Improve
Per-document-type max_tokens
We currently allocate 2,048 max output tokens for all document types. A simple receipt rarely needs more than 400 tokens. A bank statement with 200 transactions might need 2,000. Dynamic allocation based on document type and page count would reduce costs on simple documents without risking truncation on complex ones.
Batch API for non-real-time workloads Batch APIs process requests asynchronously (typically within 24 hours) at roughly half the standard price (provider figures as of April 2026). We've since shipped this for Pro and Enterprise plans — large non-real-time batches are routed to the provider's Batch API, which roughly halves processing costs on those workloads.
Per-type prompts vs prompt caching tradeoff Shorter, document-type-specific prompts would reduce input tokens by 30–60% compared to our current single shared prompt. But shorter, type-specific prompts break prompt caching — the cached prompt is shared across all document types, so each variant would start a new cache entry. We've modelled this and the caching benefit outweighs the shorter prompt benefit at our current volume. This may reverse as volume grows.
Key Takeaways
Don't send images to LLMs if you can send text. Claude is a language model — text is its native format. OCR pre-processing is cheap and fast; the cost reduction is significant.
Measure before optimising. We initially estimated costs theoretically and were off by 2x. Real measurements on real documents with real usage patterns were essential for understanding where the actual cost was going.
Confidence-based fallback is important. OCR quality varies. A fallback to the more expensive Vision approach for poor-quality scans ensures that cost optimisation doesn't compromise extraction quality on difficult documents. This same confidence-first philosophy carries through to extraction, where we surface field-level confidence scores so uncertain fields get flagged for human review rather than silently exported.
Caching compounds. Prompt caching, file hash deduplication, and OCR text caching each save a modest percentage. Together they compound — the effective cost on a cached, deduplicated, text-mode extraction is less than a third of the naive Vision approach.
If you're building document extraction and want to compare notes on cost optimisation, reach us at hey@extractbee.com.
ExtractBee is operated by MB Dokigo, Vilnius, Lithuania.