Blogs

Serverless PDF Extraction on AWS Lambda

How we moved document extraction from a pool of up to 14 GPU workers to hundreds of short-lived AWS Lambda workers, and cut a 300-page document from about ten minutes to one. A plain-language primer first, then the engineering detail.

Sai Siddharth

Sai Siddharth, Senior Platform Engineer

Oct 28, 2026

18 min read

Serverless PDF Extraction on AWS Lambda

Every answer Needl.ai gives starts with reading. Before a document can be searched, quoted or cited, each of its pages has to become text. We moved that reading step from a pool of up to 14 GPU workers to hundreds of short-lived AWS Lambda workers, and a document of more than 300 pages now takes a median of 1 minute instead of 10.6 minutes.

PDFs, Office files and spreadsheets

Up to 14 GPU workers

Started when work arrived

10.6 min

500 Lambdas at a time

Started per file, stopped when done

1 min

Median time to extract a document of more than 300 pages

Figure 1. Median time to extract a document of more than 300 pages, before and after the move to serverless.

Part one: the plain-language version. Four short sections on what changed and why. Every technical term is underlined where it first appears; hover over it, or tap it on a phone, for a definition.

What document extraction is

Document extraction turns a file into text a computer can work with: every word, table and chart label on every page, in reading order. Search, citations, red-flag checks and memos all work from that text, so nothing else can start until extraction is done. When a team connects a data room or a shared drive, extraction is the first thing that happens to each file.

Older systems used OCROptical character recognition: software that recognises the letters in a picture of a page., software that recognises the letters in a picture of a page. Today, AI Vision modelAn AI model that takes an image of a page and returns its contents as text, tables and charts included. read a page much as a person does, tables, charts and multi-column layouts included. Our pipeline sends a picture of each page to one of these models through an APIA way for one program to send a request to another over the internet and get an answer back. and gets the page's text back. Which model reads best is a separate question, covered in our benchmark of 42 PDF extraction tools. This post is about running that reading across thousands of pages at once.

A page from an annual report with a revenue table and a bar chart is read by a vision model and comes out as structured text, with the table rebuilt and the chart turned into a data table
Figure 2. What extraction produces for one page. The table keeps its rows and columns, and a chart comes out as a data table, with a one-line note only where the numbers alone do not make the point.

What changed

Our first pipeline ran OCR on GPUA graphics processor. It is the usual chip for running AI models and the most expensive part of a server to rent. servers: large machines with graphics chips, priced at $0.80 an hour and started when work arrived. We ran up to 14 of them. A long document was spread across several of them, but at most 14 pieces could be read at once, and every other file waited in line. Small files waited a median of 4.5 minutes before any work on them started.

Vision models changed the shape of the work. When a model reads the page, our worker sends the page and then waits for the answer, 2.9 seconds at the median. Waiting needs very little computing power, so a few large machines are the wrong tool for it. Many small, cheap workers that wait side by side are the right one.

Watch one 300-page document on both pipelines

Median time to searchable text | each block is one chunk

GPU pipeline6 chunks of 50 pages
10.6 min
AWS Lambda60 chunks of five pages, all at once
1 min
02468min

Figure 3. Both pipelines run at the same speed, two minutes to the second. On the GPU pipeline the document was six 50-page chunks, and most of its 10.6 minutes went on waiting for a GPU and then reading page by page. On Lambda all sixty five-page chunks start together. The times are the measured medians.

AWS LambdaAmazon's serverless service. It starts a copy of a function for each task, runs many copies at once and bills per millisecond. provides that. It starts a copy of our code for each piece of work, up to 500 at a time in this pipeline, and stops each copy when its piece is done. We pay for the milliseconds the code runs and nothing when no files arrive.

That matters because files arrive in bursts. Someone connects a shared drive, hundreds of files land at once, and then nothing happens for hours. A pool capped at 14 workers is too small for the burst, so files queue behind it.

Files arrive in bursts

Illustrative day | the same arrivals, two responses

Up to 14 GPU workersFiles queue behind the cap and are worked off for hours
Cap of 14Waiting in line

MorningMiddayEvening

With a pool capped at 14 workers, work under way is held flat at the cap while the burst of arrivals waits in line, and the backlog is worked off over the following hours.

Serverless workersCapacity follows the arrivals, up to 500 at once

MorningMiddayEvening

With serverless workers, work under way follows the arrivals exactly and nothing runs between bursts.

Files waiting Work under way Cap of 14

Illustration of the arrival pattern, not measured data

Figure 4. The same day of files against a pool capped at 14 workers and against serverless workers.

How a document is read now

Every file goes through the same five steps, whether it has 3 pages or 800.

A 300-page annual report split into 60 chunks of five pages, read by 60 workers at the same time and joined back into searchable text in about a minute
Figure 5. The same 300-page report as in Figure 3, on the new pipeline. Each chunk gets its own worker, and the pages are joined back in order once every chunk is done.
  1. A file arrives. Our ingestion service records a new job and puts one message on a QueueA managed to-do list of tasks. Workers take tasks from it, and a task that isn't finished goes back on the list.. It doesn't open the file.
  2. The file is split. A worker counts the pages and creates one task for every five pages, so a 300-page report becomes 60 tasks. Word and PowerPoint files are converted to PDF first. Spreadsheets are split by sheet.
  3. Every piece is read at the same time. Each task starts its own Lambda worker, which turns its five pages into images, sends them to the vision model and saves the text of each page.
  4. A checker watches the job. Every five seconds, a small program checks whether all of a document's pieces are done.
  5. The document becomes searchable. When the last piece is in, the pages are joined in order and added to the search index, where the document can be searched and cited.

Because the pieces run side by side, a document takes about as long as its slowest piece, and length matters much less than it used to. On Lambda, documents over 300 pages took only four times as long as documents under 20 pages.

What this means for your team

  • Documents are ready sooner. A new file becomes searchable and citable soon after it lands in a data room, whether it has 3 pages or 800.
  • One long file no longer holds up short ones. Every file is split and read in parallel, so a single 500-page agreement does not keep a two-page letter waiting.
  • One pipeline for a mixed room. PDFs, Word and PowerPoint files and spreadsheets go through the same pipeline, so a data room is handled as one batch.

See it on a data room. The AI Due Diligence page shows what Needl.ai does with the text once it is extracted: a first pass, red flags and a cited memo.

See AI Due Diligence

Ten terms to know

Most of part two builds on these ten. The other terms the post uses are defined in the glossary at the end.

Document extraction
Turning a file into plain text a computer can search, quote and analyse, page by page.
Vision model
An AI model that takes an image of a page and returns its contents as text, tables and charts included.
GPU
A graphics processor. It is the usual chip for running AI models and the most expensive part of a server to rent.
Server
A computer in a data centre that you rent, start and manage yourself.
Serverless
Running code without renting servers: the cloud provider starts machines when there is work and bills only for the time the code runs.
AWS Lambda
Amazon's serverless service. It starts a copy of a function for each task, runs many copies at once and bills per millisecond.
Worker
A program whose only job is to take a task, do it, save the result and take the next one.
Queue
A managed to-do list of tasks. Workers take tasks from it, and a task that isn't finished goes back on the list.
Chunk
A small slice of a document that one worker handles: five pages here, or one sheet of a spreadsheet.
Concurrency
How many workers are running at the same moment. Our peak was 507.

Part two: the engineering detail. From here on, the post follows a file through the pipeline, then covers what we learned, what we measured and where this design stops being the right one.

Where the GPU setup hit its ceiling

In an earlier write-up, we described scaling OCR for long PDFs with Ray on Kubernetes. A 463-page document that took 38 minutes on one GPU finished in about 10 minutes across 10 GPUs.

From 38 minutes to about one

Time to extract one long document, by pipeline generation | earlier generations | today

One GPU, OCR model463-page document38 min
Ten GPUs with RaySame documentAbout 10 min
AWS Lambda, vision model791-page document, median of 10 runs79 s

Source: our GPU write-up and our measurements. Different documents, so read the trend, not the exact ratio

Figure 6. Three generations of the same job. Spreading OCR across GPUs cut the wait to minutes; letting a model read the page and fanning out to Lambda cut it to about one.

That was a large win at the time. A year of running it showed three limits.

The previous pipeline: new files wait in one shared queue, a median of 4.5 minutes for small files, for one of up to 14 GPU workers running an OCR model on Ray on Kubernetes, then go to the indexer
Figure 7. The previous pipeline. Every file waited in one shared queue for one of up to 14 GPU workers, and an OCR model read each page.
  • Waiting for a GPU. Small files waited a median of 4.5 minutes for a GPU before any work started.
  • A fixed ceiling. Parallel work was capped by the number of GPUs: 14 workers.
  • Rising cost per page. Each page used 3.5 seconds of GPU time, and 10.8 seconds after an OCR model upgrade.

Bursty arrivals made all three worse: a pool capped at 14 workers was too small at the peak (Figure 4).

With a vision model doing the reading, a worker's job comes down to four steps: render a page as an image, send it to the model, wait for the answer and save the text. The wait is most of the time, 2.9 seconds at the MedianThe middle value: half the documents finished faster than this and half slower. and 7.7 seconds at the 90th percentile (p90)90% of documents finished within this time. It shows the slow tail that a median hides., and it needs almost no processor, memory or GPU. A GPU server waiting on replies from the network is an expensive way to wait.

Most of a worker's time is spent waiting

One page, one worker | processor and memory are almost idle during the wait

RenderSend2.9 s median, 7.7 s at p90Wait for the modelSave

Only the wait is measured, and the other widths are illustrative

Figure 8. The four steps a worker takes for each page once a vision model does the reading.

So the new design had four requirements: many small workers that can wait in parallel, workers that appear when files arrive and go away when they finish, parallel work inside a single document as well as across documents, and one pipeline for PDFs, Office files and spreadsheets.

Why Lambda and SQS fit the work

Lambda starts one worker per waiting message. It peaked at 507 workers running at once, with no ThrottlingA service limiting how many requests it accepts and asking callers to slow down., no servers to manage, no autoscaler to tune and no GPU quota to request. Billing is per millisecond, so a quiet hour costs nothing.

Amazon SQSAmazon's queue service. Each Lambda function in this pipeline reads from its own SQS queue., the queue in front of each function, takes care of the parts that are easy to get wrong. RetryRunning a failed task again, usually by putting its message back on the queue., Visibility timeoutHow long a message stays hidden from other workers while one works on it. Unfinished work reappears after it. and Dead-letter queueWhere a message goes after failing too many times, so a person can look at it. come with the service. Each function also gets its own memory, TimeoutThe longest a task may run before it is stopped. and share of the account's capacity.

How the account's 1,000 workers are shared

Reserved concurrency per function, split by queue | the rest stays free for everything else in the account

FunctionQueueMemoryTimeoutWorkers
ExtractionSplit queueSplit queue2 GB5 min100
ExtractionChunk queueChunk queue2 GB5 min500
Office conversionConversion queueConversion queue3 GB15 min100
SpreadsheetsSpreadsheet queueSpreadsheet queue4 GB15 min100
Everything else in the account200

Figure 9. Three Lambda functions share the account's capacity. The extraction function's 600 is split between its two queues, 100 for splitting and 500 for chunks, so neither can starve the other.

The platform was only half the answer. How the work is split mattered more than where it runs.

Every file becomes small, independent pieces

Whatever the file type, it is broken into units that one worker can finish in seconds. PDFs become five-page ChunkA small slice of a document that one worker handles: five pages here, or one sheet of a spreadsheet.. Word and PowerPoint files are converted to PDF first and then split the same way. Excel workbooks become one unit per sheet, with no model involved.

A 300-page report splits into page ranges, a slide deck is converted to a 40-page PDF and split the same way, and a three-sheet workbook splits into one unit per sheet; every unit goes to its own Lambda, all running at the same time
Figure 10. Every file type ends up as small units that run in parallel. Office files take one extra step; spreadsheets skip the model.

A 300-page report becomes 60 chunks running at the same time, and a three-sheet workbook becomes three. How long a document takes now depends on its slowest chunk.

Chunk size sets the ceiling

The first version used 25-page chunks. Fewer messages and less overhead seemed sensible. But a test batch of 1,618 pages produced only 73 chunks, so it could never use more than 73 of the 500 workers available. With five-page chunks, a larger batch kept all 500 busy. Chunk size decides how much parallel work is possible at all.

Chunk size sets the ceiling

500 workers available | each square is one worker | busy workers in teal

73of 500 workers busyA 1,618-page test batch made only 73 chunks, so 427 workers had nothing to do.

500of 500 workers busyWith five pages per chunk, a larger batch gave every available worker something to read.

Figure 11. Switch between the two chunk sizes. With 25-page chunks the batch could never use more than 73 workers, whatever the account allowed.

The pipeline end to end

Every document we process ends up in Elasticsearch, the Search indexElasticsearch, the engine that makes extracted text searchable. A document reaches it only when every page is ready. behind Needl.ai. The service that writes documents there, our indexer, normally extracts the text itself. For files that go through Lambda, it waits until every page is back. The numbered parts of the diagram are the sections that follow; pick a file type to trace its path.

One message in, many Lambdas out, one poller to finish

The extraction pipeline | pick a file type to trace its path

PDFWord or PowerPointExcelSame split stepOther sheetsPages, then statusSheet text,and statusReads statusReads pagesReleaseIngestion serviceCreates a job, sends one message1Split queueConversion queueSpreadsheet queueConversion LambdaLibreOffice to PDF4Sheet Lambdas × NOne sheet per run5Splitter LambdaCounts pages, sends one message per five2Extraction queueExtraction Lambdas × NRender five pages, call the vision model3Completion pollerFan-in every five seconds6IndexerMerges pages in orderElasticsearchSearchable and citableS3One text file per page or sheetPostgresJob and chunk status
  1. 1

    The ingestion service only plansCreates the job and sends one small message. It never opens the file.

  2. 2

    The splitter fans outCounts the pages and sends one message per five pages.

  3. 3

    Paying for each page onceWorkers save each page to S3 before marking the chunk done in Postgres.

  4. 4

    Office files are converted firstLibreOffice turns Word and PowerPoint files into PDFs, then the same split step runs.

  5. 5

    Spreadsheets skip the modelOne worker per sheet reads the cells directly.

  6. 6

    One checker decides when a document is doneThe poller releases a job only when every chunk is done.

  1. 1Plan

    Ingestion serviceCreates the job and marks the document as extracting in one transaction, then sends one message carrying ids only.

    Ingestion serviceCreates the job and marks the document as extracting in one transaction, then sends one message carrying ids only.

    Ingestion serviceCreates the job and marks the document as extracting in one transaction, then sends one message carrying ids only.

  2. 2Prepare

    Nothing to prepareA PDF goes straight to the splitter.

    Conversion LambdaConverts the file to PDF with LibreOffice and saves the PDF to S3. 3 GB of memory, 15-minute timeout.

    Spreadsheet LambdaOpens the workbook once, detects the real format from the file's contents and lists the sheets in 0.7 seconds.

  3. 3Split

    Splitter LambdaCounts the pages, writes one row per five-page chunk and sends one message per chunk, in batches.

    Conversion LambdaRuns the same split step in the same run: one message per five-page chunk.

    Spreadsheet LambdaRecords one unit per sheet, queues sheets 2 onwards as separate messages and keeps sheet 1 for itself.

  4. 4Read

    Extraction LambdasUp to 500 at once. Each renders its five pages, sends them to the vision model and falls back to a second model if needed.

    Extraction LambdasUp to 500 at once. Each renders its five pages, sends them to the vision model and falls back to a second model if needed.

    Spreadsheet LambdasOne per sheet. Each reads the cell values directly with python-calamine. No model call.

  5. 5Save

    S3, then PostgresOne Markdown file per page, then the chunk is marked done.

    S3, then PostgresOne Markdown file per page, then the chunk is marked done.

    S3, then PostgresOne text file per sheet, then the unit is marked done.

  6. 6Finish

    Completion poller and indexerThe poller releases the job once every unit is done. The indexer merges the files in order and writes them to Elasticsearch.

    Completion poller and indexerThe poller releases the job once every unit is done. The indexer merges the files in order and writes them to Elasticsearch.

    Completion poller and indexerThe poller releases the job once every unit is done. The indexer merges the files in order and writes them to Elasticsearch.

Figure 12. Every message carries ids only, and every worker writes its result before it reports progress. The numbers mark the parts this post walks through.

The ingestion service only plans

The ingestion service receives every new file, but it doesn't download the file or count its pages. In one database transaction it creates a JobOne run of extraction for one version of one document, with its own id. record and marks the document as extracting, so the indexer skips it. Then it sends one small MessageOne task on a queue. Ours carry ids only, such as the job, the document and the page range. that carries ids only. Everything else, such as where the file lives and which model to use, sits on the job record.

That keeps the ingestion service fast whatever the size of the file. Writing the job and the document's status in one transaction also closes a gap in which the indexer could pick up the document halfway through.

The splitter fans out

The splitter downloads the PDF once, counts its pages, writes one row per five-page chunk to the database and sends one message per chunk, in batches. This is the Fan-outSplitting one task into many that run at the same time, such as one document into 60 chunks.. Each message names the job, the document, where the file is stored, the page range and the model provider to use.

Two messages, both small

What travels on the queues | ids and page ranges, never file contents

Split message

One per file, from the ingestion service

Typesplit
Job7f3c9e…
Documenta43e9a…
Chunks go toExtraction queue

Extraction message

One per five pages, from the splitter

Typeextraction
Job7f3c9e…
Documenta43e9a…
Files3://documents/a43e9a…
Pages41 to 45
ModelThe workspace's provider

Ids only. Where the file lives and which model to use stay on the job record, so the message never goes stale.

Everything one worker needs for its five pages, so it starts work without a lookup.

Source: the messages in our pipeline, ids shortened

Figure 13. One split message per file becomes one extraction message per five pages. Neither carries the document itself.

Paying for each page once

Each chunk worker downloads the PDF, renders its five pages as images and sends them to the vision model five at a time. If the model times out, blocks the content or returns nothing, the worker retries the page on a fallback model hosted on AWS Bedrock. In a two-week sample, that happened on 8% of pages. The worker then saves each page as its own MarkdownA plain-text format that keeps headings, lists and tables. Each extracted page is saved as one Markdown file. file in S3Amazon's file storage. The original files and the text of every page are kept there. and marks its chunk done in the database.

The order of those last steps matters more than anything else in this design: model first, then storage, then the database. SQS uses At-least-once deliveryA queue's promise that every message is delivered, sometimes more than once, so workers must expect repeats., and Lambda workers do get stopped in the middle of a run. Before calling the model for a page, a worker checks whether that page's text is already in S3 and skips it if so. Because the database is updated last, it can always catch up from what is in storage.

What a retry costs

A worker stops while reading pages 41 to 45 | pick the moment it stopped

1Call the modelPaid per page
2Save each page to S3The record of what was paid
3Mark the chunk doneProgress only

What the retry does

Page 41Saved earlier, skippedRead onceRead again, paid twicePage 42Saved earlier, skippedRead onceRead again, paid twicePage 43Saved earlier, skippedRead onceRead again, paid twicePage 44Saved earlier, skippedRead onceRead again, paid twicePage 45Saved earlier, skippedRead onceRead again, paid twice

The retry skips pages 41 to 43, reads 44 and 45 once and pays nothing extra.The answer for page 44 was never saved, so the retry reads it again and pays for page 44 twice.Every page is already in S3, so the retry pays nothing and marks the chunk done.

Figure 14. Because pages are saved before the chunk is marked done, a retry pays the model again only for a page whose answer arrived in the moment before the worker stopped.

Every chunk is therefore IdempotentSafe to run twice. Running the task a second time gives the same result.: running it twice gives the same result, and each page is paid for once. The only exception is a page whose answer arrived in the moment before a worker stopped. S3 is the record of what was paid for, and the database only tracks progress.

The job id is part of every page's storage path. If a changed version of the same document arrives later, it gets a new job and a clean folder, so pages from the old version are never merged into the new text.

One folder per job

Where page text lives in S3 | upload a new version to see what changes

results

a43e9a…Annual report.pdf

7f3c9e…Job for version 1

  • 00001.md
  • 00002.md
  • 00003.md
  • ⋮
  • 00300.md

Kept apart, never merged into the new text

b81d20…Job for version 2

  • 00001.md
  • 00002.md
  • 00003.md
  • ⋮
  • 00312.md

Zero-padded names sort in page order, so the indexer merges a document by listing its folder.

The job id is in the path. A changed file gets a new job and a clean folder.

Source: our pipeline's storage layout, ids shortened

Figure 15. Each page is its own file under its job's folder. A new version of the document writes to a new folder, so old pages never mix with new ones.

Office files are converted first

Word and PowerPoint files go to a separate conversion function: a Container imageA packaged copy of a program with everything it needs to run, such as LibreOffice for Office files. that runs LibreOffice with no screen attached. It converts the file to PDF, saves the PDF to S3 and runs the same split step in the same run. From then on, an Office file is handled exactly like a PDF.

It is a separate function for two reasons. It needs more memory (3 GB) and a longer timeout (15 minutes) for large decks, and its own share of capacity means a pile of slide decks can't hold up PDF extraction. Conversion and splitting together take 2.9 seconds at the median and 12.3 seconds at the 90th percentile.

Spreadsheets skip the model

Spreadsheets aren't pages. One sheet in a workbook we tested held 128 million characters of text. Rendering that as page images for a vision model would be slow, expensive and worse at keeping the table's structure. For search, the raw cell text is enough, and any analysis that needs the real numbers reads the original file.

So spreadsheets take their own path, through python-calamine, a Python library built on a fast spreadsheet reader written in Rust. It reads .xlsx, .xls and .ods files, and it replaced a general-purpose document parser on this path. The output is plain text: a header for each sheet, then one line per row with the cells separated by tabs.

A 14-sheet workbook opened once by a first worker that reads sheet 1 and queues the other 13 sheets for 13 more workers, producing plain text with one header per sheet
Figure 16. The first worker lists the sheets, reads sheet 1 itself and queues the rest. Whole numbers come out as integers, so a search for 8337 finds the cell.

Each sheet gets its own worker. The first worker opens the workbook, records one unit per sheet, sends sheets 2 onwards back to the queue as separate messages and reads sheet 1 itself.

Splitting by sheet roughly halves the time

324 MB workbook with 14 sheets | one run reads every sheet | split by sheet, slowest sheet

Time to finish

One run27.8 s
Split by sheet14.1 s

Peak memory

One run2.09 GB
Split by sheet1.74 GB

One run Listing the sheets, 0.7 s Slowest single sheet

Figure 17. Reading a large workbook in one run against one worker per sheet.

Four details that caught us out

  • Older .xls files are read in full on open. There is nothing to gain by splitting them, so they finish in the first run, as single-sheet files do.
  • The file extension can be wrong. A .xls file renamed to .xlsx turns up more often than you would expect, so we detect the format from the file's contents.
  • Whole numbers gain a decimal. A cell showing 8337 comes back from the reader as 8337.0. We print whole numbers as integers, because someone will search for "8337".
  • One very large sheet can't be split further. An .xlsx sheet is stored as a single stream of XML, and there is no way to start reading it in the middle. One sheet per worker is as fine as the split gets.

One checker decides when a document is done

Workers never decide whether a document is finished. With 60 chunks completing at around the same moment, "am I the last one?" turns into a race very quickly. Instead, one small PollerA small program that checks something on a schedule. Ours checks every five seconds whether a document's chunks are done. is the only writer of job status. This is the Fan-inCollecting the results of those parallel tasks and deciding when the whole job is finished., and it runs every five seconds.

Because the checker releases a document on its first run after the last chunk finishes, the slowest chunk sets the finish, and the checker adds at most five seconds.

The slowest chunk sets the finish

One 100-page document, illustrative | each row is one five-page chunk

Chunk run Cold start, about 6 s Fallback model call Checker, every 5 s Slowest chunk ends, then waits for the next check

Illustrative timings shaped by our medians: 2.9 s per model call, 7.7 s at p90, 35 s end to end for 51 to 100 pages

Figure 18. Inside one job. Most chunks finish within 15 seconds; the slowest chunk and the checker's next run decide when the document is released.

The checker applies three rules on every run.

  • A job with every chunk done is marked successful and released to the indexer.
  • A job with a failed chunk is marked failed.
  • A job with no progress for 30 minutes is marked failed, so nothing waits forever.

Workers update only their own chunk, with one exception. If a message fails on its last allowed delivery, the worker fails the job at once, so a broken file doesn't sit until the 30-minute timeout. The indexer then lists the job's folder in S3, merges the pages in order (zero-padded file names keep them sorted) and indexes the text like any other document.

What we learned

More workers beat faster workers

We ran the same 100 documents twice with the same chunk size. In the second run the average Lambda call was slower, 12.5 seconds against 8.8, yet the batch finished three times faster, in 89 seconds against 265, because the peak number of busy workers rose from 52 to 195. Check how many workers are actually busy before making each one faster.

More workers beat faster workers

The same 100 documents and the same chunk size | first run | second run

Average time per call

First run8.8 s
Second run12.5 s

Each call got slower

Peak workers busy

First run52
Second run195

Almost four times as many

Time for the whole batch

First run265 s
Second run89 s

Three times faster

First run Second run

Figure 19. The second run finished three times sooner although every call took longer, because far more workers were busy.

Retries have to outlast real outages

One evening our model provider had a BrownoutA partial outage in which many requests fail for a while. Servers often reply with HTTP 503, “temporarily unavailable”. of about 25 minutes. Six in ten documents in that batch failed, and Lambda cost per page reached 4.9 times normal, nearly all of it spent on retries. Three things stacked up.

  • The AWS SDK treats an HTTP 503 reply as a temporary error rather than as throttling, so its adaptive rate limiting never started.
  • Errors came back in 0.3 to 0.5 seconds, so 500 workers retried in step with each other.
  • The queue's whole retry budget was 60 seconds, against an outage of 25 minutes.

Retries gave up long before the outage ended

One evening | the model provider's brownout | the queue's whole retry budget

Provider brownoutAbout 25 minutes
Retry budget60 seconds
0510152025 min
6 in 10documents in the batch failed
4.9xnormal Lambda cost per page, almost all of it retries
0.3 to 0.5 sfor each error to return, so 500 workers retried in step

Figure 20. A 60-second retry budget against a 25-minute brownout. Retries need to outlast the outages that actually happen.

Add up the retries at every layer and compare the total with how long real outages last. Ours didn't come close. Waiting longer after each failure, with a random extra delay (Backoff with jitterWaiting longer after each failed try, plus a random extra delay, so many workers don't retry at the same instant.), is the standard remedy and worth reading about before your first provider outage.

Give each file type its own queue and capacity

PDF extraction reserves 600 workers (100 for splitting, 500 for chunks), conversion 100 and spreadsheets 100, which is 800 of the account's 1,000 and leaves room for everything else (Figure 9). Without Reserved concurrencyA share of an account's Lambda capacity set aside for one function, so no other function can use it up., one large upload of slide decks could starve every other function.

One function, many message types

Splitting and chunk extraction are the same Lambda function, and a type field in each message decides what to do. The spreadsheet function runs the same image too. There is one image to build and one place to fix bugs, with separate queues and capacity for each job type.

Not everything needs a model

Spreadsheets made it obvious. Plain parsing is faster and cheaper, and it keeps working when a model provider has a bad day.

What we measured

The GPU column covers 1,332 Ray jobs from the year before the OCR model upgrade, and only the extraction step, since splitting the PDF was a separate job then. The Lambda column covers more than 26,000 jobs. They aren't the same documents run twice, so read the comparison as a before and after rather than a benchmark.

Faster at every document size

Seconds from job created to job done, median | GPU servers | AWS Lambda

1 to 20316 s15 s
21 to 50360 s26 s
51 to 100472 s35 s
101 to 300495 s54 s
Over 300634 s60 s

GPU servers AWS Lambda

In seconds

PagesGPU medianGPU p90Lambda medianLambda p90
1 to 203165371554
21 to 503606022693
51 to 10047274635135
101 to 30049576554173
Over 3006341,35460169

1,332 GPU jobs and more than 26,000 Lambda jobs

Figure 21. Median times by document length, with the 90th percentile in the table. Different documents and periods, so a before and after rather than a benchmark.

On Lambda, documents over 300 pages took only four times as long as documents under 20 pages. That is the fan-out doing its job. After the OCR model upgrade, the GPU median for documents over 300 pages rose to 1,888 seconds, about 31 minutes.

The largest runs

79 smedian for the largest document, 791 pages, over 10 runs60 to 242 s
34.7pages a second in the largest batch: 897 documents and 42,335 pages500 workers at peak
0.15%of 285,133 Lambda runs over 60 days ended in an errorzero throttles
2.9 smedian to convert and split a Word or PowerPoint file12.3 s at p90

Figure 22. The longest document, the largest batch and the error rate over 60 days.

Cost

Over one full week of 4,389 documents and 151,053 pages, model calls cost $260.49 and Lambda compute $12.30. That is $1.81 per 1,000 pages, and 95% of it is the model.

The model is 95% of the cost

Cost per 1,000 pages | GPU counts busy seconds only | Lambda includes the model

GPU, original OCR model$0.78
Lambda and a vision model$1.81
GPU, upgraded OCR model$2.40

GPU time at $0.80 an hour Model calls, $1.72 Lambda compute, $0.08

One week of 4,389 documents and 151,053 pages

Figure 23. Per page, a busy GPU was cheaper before the OCR model upgrade and dearer after it. Serverless bought speed and an idle cost of zero.

The GPU path used 3.51 seconds of GPU time per page before the OCR upgrade and 10.78 seconds after. At the $0.80 an hour we paid for a g6.xlarge GPU server, that is $0.78 and $2.40 per 1,000 pages, counting only the seconds a GPU was busy and none of the start-up or idle time between bursts. Per page, a busy GPU was cheaper than Lambda plus the model before the upgrade, and more expensive after it. What serverless bought was speed, and no bill when idle.

Limits and trade-offs

  • Account capacity is shared. Every Lambda function in an account and region draws from the same pool. Reserve capacity per function, or one workload will take the rest.
  • The model provider becomes the real limit. Five hundred workers with five pages each means up to 2,500 model calls in flight. Provider Rate limitThe number of requests a provider accepts per second or per minute. and brownouts now decide throughput.
  • Cold starts are real. About 9% of runs were Cold startThe few seconds a new Lambda copy needs to start up before it can take its first task., and start-up took 5.8 seconds at the median, or 6.6 seconds for the LibreOffice image. Keep heavy imports and model clients off the path that doesn't need them.
  • It isn't right for every workload. If documents can't be sent to an external model provider, or the load is steady enough to keep GPUs busy all day, a GPU cluster still makes sense.

Is serverless extraction a fit for your workload?

Tick each statement that is true for you

Tick the statements that are true for your workload to see what each one means.

What this means for you

All four apply: serverless fits. Split the work into small units, reserve capacity for each function and make every step safe to run twice.

  • Waiting uses almost no processor or memory, so many small workers waiting side by side cost less than a few large machines.
  • Workers start with the burst and stop when it ends, so you pay for the burst and nothing in between.
  • Each worker calls a hosted vision model, so there is no GPU to run, scale or keep busy.
  • Short pieces fit Lambda’s time limits, and splitting is what lets hundreds of workers run at once.

Any statement that is not true for you points to a long-running server or a GPU cluster for that work.

Source: the trade-offs in this post

Figure 24. Four questions that decided it for us. Each one you tick shows what it means for your workload.

Conclusion

The lesson from the GPU work was to structure the work first and parallelise second, and it still holds. What changed is where the heavy lifting happens. Once a model reads the page, extraction stops being a computing problem and becomes a coordination problem: splitting the work, retrying safely and knowing when everything is done. Five ideas carried this design. Each one came with a cost, and the costs are what we would tell a team starting today.

IdeaIn this pipelineWhat it costs
1Break every file into small, independent unitsFive-page chunks and one unit per sheet, so up to 500 run at onceMore messages and more status rows to track
2Let the number of workers drive speedHundreds of short Lambda runs instead of a few large serversThe model provider's rate limits become the ceiling
3Save the expensive result firstModel, then S3, then the database, with an S3 check before each pageOne extra storage check per page
4Keep one writer for job stateA checker that releases finished documents every five secondsUp to five seconds before a finished document is released
5Use a model only where you need oneSpreadsheets read directly, with no model callSearch gets plain cell text; analysis reads the original file

For teams on Needl.ai, the result is that newly added documents, long ones included, become searchable and citable sooner. If you run document extraction on large volumes and want to compare notes, get in touch.

Further reading. Using Lambda with Amazon SQS, on batching, retries and partial batch failures. Lambda reserved concurrency, on keeping one function from starving the others. python-calamine, the spreadsheet reader used for Excel files. Exponential Backoff and Jitter, on the AWS Architecture Blog. The Write Last, Read First Rule, the same ordering idea applied to keeping two databases in step. Scaling AI Document Processing on Kubernetes with Ray and KubeRay, the GPU side of this story.

Glossary

The ten core terms are defined in part one. These are the rest.

Reading documents

OCR
Optical character recognition: software that recognises the letters in a picture of a page.
API
A way for one program to send a request to another over the internet and get an answer back.
Markdown
A plain-text format that keeps headings, lists and tables. Each extracted page is saved as one Markdown file.

Where the work runs

Cluster
A group of servers managed together. Our GPU workers ran on a Kubernetes cluster, with Ray spreading the work.
Reserved concurrency
A share of an account's Lambda capacity set aside for one function, so no other function can use it up.
Cold start
The few seconds a new Lambda copy needs to start up before it can take its first task.
Container image
A packaged copy of a program with everything it needs to run, such as LibreOffice for Office files.

How work moves

Amazon SQS
Amazon's queue service. Each Lambda function in this pipeline reads from its own SQS queue.
Message
One task on a queue. Ours carry ids only, such as the job, the document and the page range.
Fan-out
Splitting one task into many that run at the same time, such as one document into 60 chunks.
Fan-in
Collecting the results of those parallel tasks and deciding when the whole job is finished.
Poller
A small program that checks something on a schedule. Ours checks every five seconds whether a document's chunks are done.

Where results are kept

S3
Amazon's file storage. The original files and the text of every page are kept there.
Database
Postgres, which tracks each document's job and the status of every chunk.
Search index
Elasticsearch, the engine that makes extracted text searchable. A document reaches it only when every page is ready.
Job
One run of extraction for one version of one document, with its own id.

When things go wrong

Retry
Running a failed task again, usually by putting its message back on the queue.
Idempotent
Safe to run twice. Running the task a second time gives the same result.
At-least-once delivery
A queue's promise that every message is delivered, sometimes more than once, so workers must expect repeats.
Visibility timeout
How long a message stays hidden from other workers while one works on it. Unfinished work reappears after it.
Dead-letter queue
Where a message goes after failing too many times, so a person can look at it.
Timeout
The longest a task may run before it is stopped.
Throttling
A service limiting how many requests it accepts and asking callers to slow down.
Rate limit
The number of requests a provider accepts per second or per minute.
Brownout
A partial outage in which many requests fail for a while. Servers often reply with HTTP 503, “temporarily unavailable”.
Backoff with jitter
Waiting longer after each failed try, plus a random extra delay, so many workers don't retry at the same instant.

Measuring

Median
The middle value: half the documents finished faster than this and half slower.
90th percentile (p90)
90% of documents finished within this time. It shows the slow tail that a median hides.
Throughput
Work finished per unit of time, such as pages per second.

FAQs

What is serverless document extraction?

Serverless document extraction turns PDFs, Office files and spreadsheets into searchable text with short-lived cloud functions instead of servers you keep running yourself. Each file is split into small pieces, each piece is read by its own function, and every function stops when its piece is done. Needl.ai runs this on AWS Lambda, where a document of more than 300 pages takes about a minute.

What is AWS Lambda?

AWS Lambda is Amazon's service for running code without managing servers. Lambda starts a copy of a function for each event, such as a message arriving on a queue, runs many copies at once and bills for each millisecond of running time. When nothing is happening, nothing runs and nothing is billed. Needl.ai's extraction pipeline peaked at 507 copies running at the same time.

Why use a vision model instead of OCR?

OCR recognises characters but often loses a page's structure: table rows, chart labels and multi-column layouts. A vision model reads the page as an image and returns its text with that structure intact. In Needl.ai's benchmark of 42 extraction tools, a small group of vision models led on every quality measure, while classic text extractors kept the words but lost the tables.

When do GPU servers still make sense?

GPU servers make sense when the load is steady enough to keep them busy all day, or when documents can't be sent to an external model provider. Bursty workloads suit serverless better: Needl.ai's extraction traffic arrives in large batches with quiet hours between them, so a pool capped at 14 GPU workers was too small at peak.

What happens if a worker fails halfway through a document?

The queue delivers the message again and another worker picks up the chunk. Because the text of each page is saved before the chunk is marked done, the retry skips pages already saved and pays only for the rest. In Needl.ai's pipeline, a checker marks the document failed if a chunk keeps failing or nothing has moved for 30 minutes.

How are spreadsheets handled?

Spreadsheets skip the vision model. A spreadsheet reader extracts the cell values as plain text, with one header per sheet and one line per row, and each sheet gets its own worker. On a 324 MB workbook with 14 sheets, splitting by sheet cut the time in Needl.ai's tests from 27.8 seconds to about 14 seconds: 0.7 seconds to list the sheets, then 13.4 seconds for the slowest sheet.

Does Needl.ai train models on my documents?

No. Needl.ai does not train models on customer data. Each page is read by our primary vision model through its provider's paid API, whose terms exclude prompts and responses from training. If that model times out, blocks a page or returns nothing, the page is retried on a fallback model hosted on AWS Bedrock, where AWS does not train on the data and the model's maker has no access to it. Needl.ai runs as SaaS, as a dedicated instance or inside the customer's own AWS or Azure account. See Trust and security for the details.

EngineeringDocument extractionPDF extractionAWS LambdaServerlessVision models

Keep reading

Related on Needl.ai: Benchmarking PDF extraction tools, the technical note

See it on your own documents

Bring the filings, reports or data room your team works with, and see Needl.ai answer with a source on every claim.

Book a demo