Run Tesseract as an API
or self-hosted service.
Wrap Tesseract in an HTTP service with confidence: a production FastAPI endpoint, process-pool concurrency, a Docker image with language packs and health checks, scaling and backpressure, and serverless deployment — with privacy and cost trade-offs against managed OCR APIs.
TL;DR — a minimal service
Wrap the CLI or the pytesseract binding behind HTTP. The image never leaves your infrastructure.
pip install fastapi uvicorn python-multipart pytesseract pillow
# install the engine + language packs first (see the install guide)
uvicorn app:app --host 0.0.0.0 --port 8000Then call it: curl -F "file=@invoice.png" "http://localhost:8000/ocr?lang=eng". Tesseract itself is installed per the installation guide.
Design decisions first
Three choices shape the whole service.
| Decision | Options | Guidance |
|---|---|---|
| Sync vs async | Blocking request vs job queue | Sync for single images under a few seconds; async job IDs for large or batched documents. |
| Input | Multipart file upload vs base64 JSON | Prefer multipart for binary images; base64 only when the client cannot upload files (adds ~33% size). |
| Output | Plain text, JSON, hOCR/ALTO, searchable PDF | Return JSON by default; offer hOCR for layout and a PDF download URL for archives. |
One primary intent per endpoint. Keep /ocr returning text, and add separate endpoints for PDFs and layout data rather than overloading one response.
A production Python service (FastAPI)
Validation, size limits, a timeout, and OCR off the event loop.
import asyncio, io
from concurrent.futures import ProcessPoolExecutor
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
from PIL import Image
import pytesseract
app = FastAPI(title="Tesseract OCR API")
pool = ProcessPoolExecutor(max_workers=4)
MAX_BYTES = 10 * 1024 * 1024 # 10 MB
LANGS = {"eng", "deu", "fra", "spa"}
def _ocr(data: bytes, lang: str) -> str:
image = Image.open(io.BytesIO(data))
return pytesseract.image_to_string(image, lang=lang)
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/ocr")
async def ocr(file: UploadFile = File(...), lang: str = Query("eng")):
if lang not in LANGS:
raise HTTPException(400, "Unsupported language")
data = await file.read()
if len(data) > MAX_BYTES:
raise HTTPException(413, "File too large")
loop = asyncio.get_running_loop()
try:
text = await asyncio.wait_for(loop.run_in_executor(pool, _ocr, data, lang), timeout=30)
except asyncio.TimeoutError:
raise HTTPException(504, "OCR timed out")
return {"text": text, "lang": lang}Key details: allow-list languages, enforce a byte limit before decoding, run OCR in a process pool, and cap the wall-clock time. See the Python guide for the full pytesseract API.
Containerize it
Bake Tesseract and the language packs into the image so replicas are identical.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu \
libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
HEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]Add only the language packs you need to keep the image small. The install guide covers Docker and tessdata placement in more depth.
Concurrency & scaling
OCR is CPU-bound — parallelise with processes, not threads.
- Process pool or CLI subprocesses: Python's GIL means a thread pool will not speed up OCR. Use
ProcessPoolExecutoror a pool oftesseractsubprocesses. - Workers vs replicas: run a few uvicorn workers per container and scale replicas horizontally behind a load balancer.
- Queueing: for bursts or long documents, accept the upload, return a job ID, and process from a queue (Celery, RQ, or a broker).
- Backpressure: cap the queue and reject with
429when saturated rather than letting latency spiral. - Resource limits: pin CPU/memory per worker and set an OCR timeout so one bad image cannot stall a replica.
Security & privacy
Self-hosting is the main reason teams pick Tesseract.
- No data egress: images stay inside your network — important for legal, medical and financial documents.
- Validate input: check MIME type and magic bytes, not just the extension; decode in a try/except.
- Limit resources: max upload size, per-request timeout, and a memory ceiling to resist decompression bombs.
- Authenticate: put an API key or gateway in front; OCR endpoints are expensive to abuse.
- Strip metadata: if you persist files, remove EXIF and delete originals on a schedule.
Serverless & container platforms
- Container runtimes: Cloud Run, AWS Lambda container images and Fly.io all run a Tesseract image without managing servers.
- Cold starts: a Tesseract image plus language packs is large, so cold starts are slow — keep one instance warm for latency-sensitive traffic.
- Limits: raise memory and timeout; a small default timeout will kill large documents.
- Ephemeral disk: write scratch files to
/tmponly; treat the filesystem as disposable.
Self-hosted Tesseract vs managed OCR APIs
| Dimension | Self-hosted Tesseract | Managed OCR API |
|---|---|---|
| Cost | Fixed infrastructure; no per-page fee | Pay per request/page; scales with volume |
| Privacy | Documents never leave your network | Documents sent to a third party |
| Accuracy | Strong on clean print; tunable per domain | Often stronger on hard scans, handwriting |
| Control | Full: versions, languages, custom models | Vendor roadmap and quotas |
| Ops | You run it: scaling, upgrades, monitoring | Fully managed |
Many teams self-host Tesseract for high-volume, privacy-sensitive work and fall back to a managed API only for the hard minority of documents.
API FAQ
How do I run Tesseract OCR as an API?
Wrap the CLI or pytesseract in FastAPI/Flask, expose POST /ocr, and containerize it. Images stay in your infrastructure.
Threads or processes?
Processes. OCR is CPU-bound and the GIL blocks thread parallelism; use ProcessPoolExecutor or CLI subprocesses.
Is a self-hosted API private?
Yes — uploaded images are processed locally and never sent to a third party, unlike managed OCR APIs.
Can it run serverless?
Yes, on container platforms like Cloud Run, Lambda container images or Fly.io; mind cold starts and image size with language packs.
What formats can I return?
Plain text, JSON with confidence, hOCR/ALTO, or a searchable PDF via a download URL.