Tesseract OCR on AWS Lambda: layer or container, tested
7.3 s
for 10 scanned pages at 1,769 MB
No package, and a layer hits glibc
The Lambda image for Python 3.13 runs Amazon Linux 2023. Its package manager has no Tesseract:
error: No package matches 'tesseract'
A layer with Debian's Tesseract is 50 MB, under the 250 MB limit. It still fails on the older glibc:
/opt/bin/tesseract: /lib64/libc.so.6: version `GLIBC_2.38' not found (required by /opt/bin/tesseract)
A container image skips both problems. It can hold any Linux, up to 10 GB.
A container image that works
Start from Debian, add Tesseract with apt, and add the AWS runtime interface client:
FROM python:3.13-slim-trixie
RUN apt-get update \
&& apt-get install -y --no-install-recommends tesseract-ocr poppler-utils \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir awslambdaric boto3
ENV OMP_THREAD_LIMIT=1
WORKDIR /var/task
COPY app.py .
ENTRYPOINT ["python", "-m", "awslambdaric"]
CMD ["app.handler"]
450 MB image size
OMP_THREAD_LIMIT=1 keeps Tesseract on one thread. Two
threads were slower at 1,769 MB: 8.8 s.
The handler: S3 in, text out
The PDF comes from S3, since Lambda caps a request at 6 MB. The text goes back as .txt:
import glob
import os
import subprocess
import tempfile
import urllib.parse
import boto3
s3 = boto3.client("s3")
def handler(event, context):
rec = event["Records"][0]["s3"]
bucket = rec["bucket"]["name"]
key = urllib.parse.unquote_plus(rec["object"]["key"])
with tempfile.TemporaryDirectory() as d:
pdf = os.path.join(d, "in.pdf")
s3.download_file(bucket, key, pdf)
subprocess.run(["pdftoppm", "-r", "300", "-gray", pdf, d + "/p"], check=True)
pages = sorted(glob.glob(d + "/p-*.pgm"))
text = subprocess.run(
["tesseract", "stdin", "stdout", "--dpi", "300"],
input="\n".join(pages),
capture_output=True,
text=True,
check=True,
).stdout
s3.put_object(Bucket=bucket, Key=key + ".txt", Body=text.encode())
return {"key": key, "pages": len(pages), "words": len(text.split())}
Memory sets the speed
Lambda gives CPU in step with memory: one vCPU at 1,769 MB. I gave each test container the same share.
| Memory | 10 scanned pages, cold |
|---|---|
| 128 MB | 291 s |
| 512 MB | 36.7 s |
| 1,024 MB | 13.4 s |
| 1,769 MB | 7.3 s |
| 3,008 MB | 7.3 s |
Every size read the same 1,879 of 1,955 true words.
Peak memory was 169 MB. One thread cannot use more than one vCPU, so 3,008 MB gained nothing.
Three errors and their fixes
| Error | Fix |
|---|---|
(404) when calling the HeadObject operation |
S3 sends "March scan.pdf" as March+scan.pdf. Decode it with unquote_plus. |
[Errno 30] Read-only file system |
Only /tmp can be written. Use tempfile, as above. |
Task timed out after 3.00 seconds |
3 s is the default. Raise it. The killed run also left its pages in /tmp. |
Give the trigger a .pdf suffix filter. The handler writes .txt to the same bucket, and AWS warns that this can loop.
Or skip the image: call an OCR API
This function needs no layer and no image, only Python's urllib. It sends a PDF URL to our OCR Actor:
import json
import os
import urllib.request
API = "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items"
def handler(event, context):
req = urllib.request.Request(
API,
data=json.dumps({"documentUrls": [event["url"]]}).encode(),
headers={
"Authorization": "Bearer " + os.environ["APIFY_TOKEN"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=290) as r:
item = json.load(r)[0]
return {"pages": item["pagesConverted"], "markdown": item["markdown"]}
353 of 381 words, 2 scanned pages
26 MB peak, in a 128 MB function
Tesseract in the container found 355 on the same pages. Set the function timeout above the 290 s wait in the code.
How I tested it
- No AWS account: each function ran in Docker under the AWS Lambda Runtime Interface Emulator 1.38, on an Apple Silicon Mac (arm64).
- Lambda-like limits: a read-only root, /tmp on disk, a non-root user, memory with no swap, and CPU as memory over 1,769 MB. The CPU share is a model, not a Lambda measurement.
- S3 was a local MinIO. Each run was a cold start in a fresh container, sent the event S3 sends for "March scan.pdf".
- Scan: the 10-page PDF of OCR in n8n (proofread Wikisource scans, no text layer). Scored with the tessdata test scorer.
- Debian trixie: Tesseract 5.5.0, Poppler 25.03.0, awslambdaric 4.2.3, boto3 1.44.0.
- The Actor: one real run from the AWS base image for Python 3.13, at 128 MB.
What I did not test
- A real AWS account: cold starts, the 6 MB cap and billing.
- x86_64 functions. I built and ran arm64 only.
- A layer built from source on Amazon Linux 2023.
- Other languages, photos, and PDFs that already have text.
Sources
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Can I install Tesseract with dnf on AWS Lambda?
No. The Lambda image for Python 3.13 runs Amazon Linux 2023, and dnf answers: No package matches 'tesseract'. Build a container image from Debian instead.
How much memory does Tesseract need on Lambda?
Peak use was 169 MB, but Lambda gives CPU in step with memory. 1,769 MB buys one full vCPU, and more did not help one thread.
Why does Lambda say Read-only file system?
Only /tmp can be written. Download the PDF and render the pages in a tempfile folder, which is under /tmp, as the handler on this page does.