TailSpend Docs

Receipt OCR performance comparison — 21 October 2026

Issue #694. Implemented replacement: PP-OCRv6 small images + Ministral 3 8B, with improved prompting/schema/normalization. PDFs now render locally through PDFium and use the same OCR; Textract is removed from the serving path. Four new calls using saved OCR returned 4/4 exact totals and complete item-cost lists, no truncation, and 1.384 s median model-stage latency. This is a development sample, not a production accuracy estimate. See the implementation follow-up below.

The original ten paid calls cost an estimated $0.00415208 at standard rates; the four new Ministral calls add $0.00085515. Cumulative reservations are $0.077, under the unchanged $0.08 cap and strict $0.10 testing limit, with $0.00500723 in full-rate service-cost estimates before cache discounts. This is not a settled AWS bill. No completed calls were repeated, ledger reset or reservations released. No Lambda, Step Functions or production changes were deployed. The original comparison and its limitations are preserved below.

Rollout update — 22 October 2026: deployment is authorized through the repository's normal dev → beta → production pipeline. Deployment status must be checked in GitHub Actions; the measurements and historical deployment statements in this report describe the evaluation at the time it ran. No additional paid inference tests are included in the rollout, and the persistent budget ledger remains unchanged.

Production follow-up — reviewed 24 October 2026: rollout completed, but two real cold OCR invocations took 32–40 seconds, and a reported item mismatch equals an explicitly included VAT disclosure. The four-image development score did not establish production accuracy or Lambda cost savings. See the final follow-up for measured Lambda reports, a conservative tax correction and remaining limits.

Recovery and method

The original handoff archive was unavailable in the new environment. Its report, runner, README, dependency versions, model hashes, and console results were recovered from the earlier chat, Assess low-cost OCR options for 694 (01a15d3a-167b-7604-ab19-6efe32c86076). The transcript establishes that the prior AWS credential preflight failed before opening the ledger; no paid request or reservation had occurred. This is recovery of an unexecuted paid evaluation, not a reset of an existing spent ledger.

The original full local JSON files could not be recovered. The saved one-thread and two-thread results contain the recovered summaries, rounded per-receipt timings, and provenance; individual timing repetitions are unavailable. The completed benchmark was not repeated. Missing OCR text was regenerated from the same pinned images and hash-verified weights; all saved original amount tokens and final 28 lines match on every receipt. See text and hashes and verbatim recovery evidence.

AWS SSO device authorization authenticated the documented account and role. Paid calls ran serially from a local Linux x64 Node process, against us-east-1, using the production structurer at repository 643edcb. Every send reserved its allowance on disk first, with SDK retries disabled. Each route stopped at its first truncation. Thus both Gemma 3 routes attempted receipts 000–001 only; Gemma 4 attempted all four. Unattempted receipts are not failures or successes. Results include single calls, not repeated samples or a production accuracy/p95 estimate.

The Gemma 3 requests used the production prompt/schema, temperature 0, and max_tokens: 2048. Gemma 4 used Mantle /openai/v1/responses, the image plus adapted production prompt/schema, temperature 1, top-p 0.95, high reasoning, and max_output_tokens: 2048. Different models/API settings are part of the routes being compared; the test does not isolate OCR quality from parser quality. Direct image output passed through the same normalizer with empty independent OCR text; no generated text was substituted as independent repair evidence.

What actually ran

PP-OCRv6 small through RapidOCR 3.10.0 / ONNX Runtime 1.24.4 ran against four public receipt images. The deterministic selection was receipts 000–003 from this SROIE mirror, pinned at 27be4271b251c256f695acbade9a801bffe85994. Images are 439–463 pixels wide and 933–1,013 pixels tall, approximately 84–122 KiB each. They include faint thermal print, handwritten totals, discounts, and rounding. These four Malaysian receipts are not a representative multilingual/quantity/PDF eval set.

The machine is Linux x86_64 with a cgroup CPU quota of two cores. This is neither deployed Lambda nor Graviton. Both detector and recogniser are the bundled v6 small models, totalling 29.7 MiB; hashes are saved in the raw reports. Orientation classification is disabled, detector minimum-side limit is 736, and preprocessing maximum side is 2,000 pixels.

Each thread configuration ran four fresh Python subprocesses, four separate warm-up calls, and twelve scored warm calls (three per receipt). Fresh-process timing includes interpreter/import/first model load/OCR. It is not a measured Lambda cold start. Warm statistics below describe these twelve calls, not a population p95.

Configuration Median warm OCR Warm min–max Fresh process + first OCR min–max Peak process RSS
One inference thread 2.635 s 2.130–3.281 s 2.842–3.695 s 401.9 MiB
Two inference threads 1.822 s 1.573–2.116 s 2.600–3.044 s 403.6 MiB

Per-receipt medians:

Receipt Printed total (MYR) One thread Two threads Printed total token in OCR
000 9.00 2.254 s 1.733 s Yes
001 60.30 2.666 s 1.851 s Yes
002 33.90 2.605 s 1.793 s Yes
003 80.90 3.227 s 2.034 s Yes

OCR output was stable across warm repetitions. Printed purchase prices and the -5.59 discount are present, but this is text evidence, not correct expense-draft extraction. Receipt 000 places a handwritten 9.00 after CHANGE, followed by 100 for the printed 1.00; downstream selection can still be wrong. Receipt 003 renders the time as 1231.00, which a parser must not treat as an item price. Dataset transcriptions also contain typos and duplicate lines, so the recorded transcript edit distances are diagnostics.

Two threads improved median latency by about 31% on this host. The observed memory suggests the OCR component can fit within 1 GiB, but Lambda allocates CPU with memory and this does not establish 1 GiB Lambda performance. The unpruned installed Python dependency directory is 426.3 MiB, exceeding the 250 MB unzipped Lambda ZIP/layer limit. Plan a CPU container or measure a carefully trimmed package; do not equate 29.7 MiB of weights with the complete deployment size.

Live extraction correctness

Raw provider content, normalized drafts, latency, and usage are preserved unchanged. The offline summary separates raw copying from normalized results.

Route Attempts Valid raw JSON Exact normalized total Exact complete item-cost list Truncations
Textract → Gemma 3 4B 2 1/2 0/2 0/2 1/2
PP-OCRv6 small → Gemma 3 4B 2 1/2 0/2 0/2 1/2
Image → Gemma 4 E2B 4 4/4 3/4 1/4 0/4

An exact total matches the printed final charged value in MYR minor units. The item metric compares multisets of all purchase, discount, and nonzero rounding amounts; zero adjustments, subtotals, payment/change, and handwritten copies of totals are excluded. It does not score item labels or ordering. Gold was fixed before inference and checked against all four source images. Currency MYR was supplied as context, so matching it is not independent currency-recognition evidence. Gemma 4 returned literal RM on 001, which the normalizer replaced with group MYR; only three of its raw currency strings were MYR.

Receipt Expected total Textract + Gemma 3 PP-OCRv6 + Gemma 3 Direct Gemma 4
000 9.00 Total absent; BHD currency; 9.00 item becomes 9,000 BHD minor units Same currency/scale problem; additionally includes cash as a purchase Correct total and cost; uses product name instead of merchant
001 60.30 Truncated, invalid JSON; currency-only fallback Truncated, invalid JSON; currency-only fallback Returns pre-rounding 60.31; pairs lamp with 10.00, omits 55.90 purchase and -0.01 rounding
002 33.90 Not attempted after route stop Not attempted after route stop Correct total; invents another 3.02 purchase from a product code; omits -0.02 rounding
003 80.90 Not attempted after route stop Not attempted after route stop Correct total and 80.91 purchase amount; omits -0.01 rounding; selects overlaid name as merchant

There are separate model and application faults:

These results reject a production switch with either tested configuration. They also expose problems in the current baseline; retaining its deployment is not an endorsement of its measured accuracy. The small, public, similarly formatted Malaysian sample cannot establish broader success rates.

Latency: measurements and limits

Seconds, one cloud call per attempted route/receipt:

Receipt Textract + Gemma 3 PP-OCRv6 + Gemma 3 Direct Gemma 4
000 2.713 3.445 15.198
001 12.061, truncated 12.540, truncated 17.033
002 Not attempted Not attempted 16.008
003 Not attempted Not attempted 16.266

Textract totals add measured OCR and model time; its OCR calls took 1.113 and 1.362 seconds. PP totals add the previous one-thread OCR median (rounded to milliseconds) to the newly measured Gemma time, not a deployed end-to-end measurement. Model-only medians were 6.150 s and 5.533 s across the two attempts, including truncation, versus 16.137 s for four direct-image attempts. These are not medians of correct successful extractions. On the two paired images, Gemma 4's median was 16.115 s; all-four and two-receipt populations must not be silently compared as equally representative.

The two-thread OCR results could reduce the local component, but have not been combined with another paid model run. Network region, cold connections, prompt caching, server load, different prompts, and reasoning settings may affect latency. No throughput, concurrency, tail latency, or Lambda cold start was measured.

Memory and Lambda suitability

Route Observed memory Lambda assessment
Textract + Gemma 3 Shared cloud-evaluation Node process peaked at 99.7 MiB across all routes; service memory invisible Existing worker is 1,024 MiB ARM64. Fits existing packaging, but its whole allocation bills while awaiting both services.
PP-OCRv6 + Gemma 3 Original standalone Python OCR peak 401.9–403.6 MiB; 29.7 MiB model files CPU feasible candidate; Node plus Python, image/PDF decoding, larger pages, and extensions need additional headroom. ARM64 runtime and CPU performance remain untested.
Direct Gemma 4 Same shared Node-process observation; no local model loaded; remote model memory invisible Native OCR packaging unnecessary, but downloaded/base64/decoded images increase memory. A smaller adapter may suffice; minimum safe Lambda memory was not measured.

Do not add independently measured process peaks and present the sum as a measured combined Lambda peak. The original unpruned Python dependency directory was 426.3 MiB, larger than the 250 MB unzipped ZIP/layer allowance. A pinned CPU container is the plausible PP-OCR packaging route; container images allow up to 10 GB uncompressed. CPU allocation scales with memory (1,769 MB provides one vCPU equivalent), so the two-thread x86 timing cannot be assumed at 1 GiB on Graviton. Native wheel compatibility and real billed duration still need verification. These are ordinary Lambda limits, not measurements of a deployed OCR function. Self-hosting Gemma was not tested; all compared language models are remotely hosted on Bedrock.

Pricing and cumulative budget

Verified us-east-1 standard rates: Textract price-list evidence is $0.0015/page for the first million pages; Bedrock pricing is $0.04/million input and $0.08/million output tokens for both models. Actual usage, including returned image/output token counts, replaces the old 1,000/1,000 illustration. The estimates below charge all input at full rate even when a response reports cached tokens. AWS billing/free-tier/cache discounts were not queried. No S3 upload, Lambda execution, or Step Functions execution was used.

Route Input / output tokens in this run Service-cost estimate for run Service cost per 1,000 attempts With illustrative 1 GiB ARM waiting/compute worker
Textract + Gemma 3, n=2 1,951 / 2,174 $0.00325196 $1.626 $1.725
PP-OCRv6 + Gemma 3, n=2 1,967 / 2,223 $0.00025652 $0.128 $0.235
Direct Gemma 4, n=4 5,248 / 5,421 $0.00064360 $0.161 $0.376

The final column is a scenario, using each route's arithmetic mean measured component durations at ARM Lambda's $0.0000133334/GB-second plus $0.20/million requests. It transplants x86/network timings into ARM pricing; it is not observed Lambda billing. It excludes initialization, validation, storage, logs, orchestration, retries, free tiers, and taxes. Truncated/incorrect attempts are included, so these figures are not costs per correct receipt. The n=2 OCR routes and n=4 direct route also have different receipt mixes.

Holding 1 GiB for Gemma 4's measured wait makes direct extraction more expensive than local OCR in this scenario despite eliminating Textract. Allocating 2 GiB to local OCR would double compute cost at unchanged duration; improved CPU speed might offset some of it but must be measured. Merely removing Textract does not establish the cheapest complete pipeline.

The canonical cumulative ledger contained two $0.0015 Textract reservations, eight $0.0055 model reservations, and $0.008 ancillary allowance at the original run’s conclusion: $0.055 retained. The follow-up below appends four new reservations, taking the same ledger to $0.077. The original /workspace/receipt-ocr-694/budget.json points to that same file. It enforces a fixed $0.08 ceiling. The follow-up used $0.022 of the original $0.025 headroom for new request IDs; no completed request was repeated and the original token caps were not raised. Keep this ledger with all future continuations. Both output reports reject overwriting attempted results and duplicate ledger IDs reject repeat sends.

Gemma 4 was asked for high reasoning. The API reported zero separately counted reasoning tokens on each response; total output usage above is retained as returned. That field alone does not validate how internal computation is billed, and no hidden tokens were added or subtracted from the price calculation.

Step Functions and idle 1 GiB compute

Step Functions saves Lambda waiting cost when it calls the remote service itself. Wrapping the current waiting Lambda preserves its compute charge. The optimized Bedrock integration can call Gemma 3 InvokeModel; keep OCR and validation/finalization in short functions, pass durable object references, and let the workflow await Bedrock. The integration supports S3 input/output for large payloads. Synchronous Textract images can use the AWS SDK integration and an S3 object reference; filter large OCR responses before carrying them into later states.

Standard is attractive for asynchronous Textract PDFs: start once with a stable idempotency token, wait for its completion notification through a callback, then fetch/validate results in a short task. This avoids the existing 500 ms polling loop in a 1 GiB Lambda. Six transitions is an illustrative happy path, not a counted implementation. Every polling iteration/retry adds transitions; shorter waits do not automatically make Standard cheaper.

Waiting/orchestration costs per 1,000 jobs, excluding service calls, short Lambda tasks, free tiers, logs, and connection/secret overhead:

Wait duration 1 GiB ARM Lambda compute only Standard, six transitions Express, assumed 64 MiB
10 seconds $0.1333 $0.1500 $0.0114
30 seconds $0.4000 $0.1500 $0.0323
16.126 seconds, direct-image observed mean $0.2150 $0.1500 $0.0178

AWS Step Functions prices used here are $0.000025/Standard transition and Express $1/million starts plus $0.00001667/GB-second, with duration and memory billing rounding. At six transitions, Standard breaks even against 11.25 seconds of 1 GiB ARM waiting before other tasks/costs. The measured 7.387-second baseline remote wait would cost $0.0985/1,000 in Lambda compute, already below $0.15 in Standard transitions; the local route's 5.533-second model wait is $0.0738/1,000. Express scenarios for those waits are $0.0087 and $0.0068/1,000, plus the CPU OCR and short tasks. 64 MiB is a billed-workflow-memory assumption, not the local model memory size.

Gemma 4 E2B is Mantle-only and cannot simply replace the model ID in bedrock:invokeModel. Our successful SigV4 request proves the evaluation client, not a Step Functions integration. A direct HTTP Task requires an EventBridge connection, has a 60-second timeout, and does not inherit the SDK client's SigV4 signing. Bedrock supports bearer API keys, which could be a connection design to validate, with rotation and scoped permissions. The workflow's 256 KiB state payload limit also matters: these small inline images fit, but normal large receipts may not. Larger-image transport and API authentication need an explicit design; Bedrock InvokeModel's S3 extension does not establish S3 support for a Mantle HTTP Task.

A smaller SigV4 Lambda adapter is an alternative: at a hypothetical 128 MiB, 16.126 seconds of waiting compute is about $0.0269/1,000 calls, plus invocation and workflow costs. It still pays for waiting, and 128 MiB suitability is unmeasured. Express has a five-minute limit and no callback or .sync job-run pattern; asynchronous Express has at-least-once execution semantics. Preserve leases, completed-step checkpoints, and paid-call deduplication regardless of workflow choice. Standard callbacks suit long PDF waits; Express direct service calls are the more promising cost experiment for short image jobs. No workflow was created or benchmarked in this task.

Published evidence is a separate category

The research evaluation retains sources and limitations. Paddle's vendor evaluation reports PP-OCRv6 small recognition accuracy of 81.3% and 0.61 s/image on Xeon/ONNX; those are neither receipt-field accuracy nor Lambda timing. Google's E2B OmniDocBench 1.5 edit-distance score (0.290) measures broader document parsing. The independent Fraunhofer receipt result of 53.79% exact fields is for direct Gemma 3 image input, not either OCR→Gemma route tested here. These published scores cannot be combined into a common leaderboard or used to override the observed errors.

Saved evidence and verification

Budget tests verify cap enforcement, persisted reservations, duplicate/concurrent run rejection, and corrupt-ledger refusal. Offline analysis checks every saved model/OCR result against its reservation, token bounds, raw JSON, totals, complete cost multisets, and standard-rate cost calculations. No inference was repeated to validate this report. Formatting and local evidence links are checked separately.

Original comparison recommendation

Do not replace Textract with either tested configuration yet. PP-OCRv6 small is a credible low-memory CPU OCR component and retains the necessary text amounts, but pairing it with the unchanged Gemma 3 structurer fails the same quality checks as the existing baseline. Direct Gemma 4 integrates successfully and removes native OCR packaging, but 3/4 totals, 1/4 complete amount lists, merchant errors, and a 16-second median do not support production adoption.

Prioritize the shared extraction contract: fix currency evidence so SDN BHD cannot select BHD, exclude cash/tender from purchases, reject truncated or incomplete drafts, and validate totals plus rounding/discount/item arithmetic. Then evaluate a successor structurer using these saved OCR inputs and a broader, private labelled set. AWS lists Gemma 3 4B as legacy with EOL on 30 March 2027, so changing only the OCR engine would leave a near-term model migration. Retain Textract for PDFs until a bounded rasterization alternative is measured.

For orchestration, assess Express with direct supported service calls for short image jobs and Standard callbacks for asynchronous PDFs; compare measured wait time to transition costs before choosing. Treat a smaller remote-call adapter as another cost option. Keep issue #694 open: deployment should follow corrected extraction, representative multilingual/quantity/large-image/PDF testing, real ARM64 packaging and cold-start measurements, and a validated orchestration path. The present evaluation is complete within its fail-stop budget; none of these follow-ups were deployed or silently substituted into the measured results.

Implemented follow-up: PP-OCRv6 small + Ministral 3 8B

After the original comparison, the replacement was implemented in source: PP-OCRv6 small for JPEG/PNG through a dedicated Python CPU container Lambda, Ministral 3 8B Instruct (mistral.ministral-3-8b-instruct) for structuring, and revised system instructions/schema/normalization. PDFs retain the existing asynchronous Textract path. No production or evaluation Lambda was deployed.

The prompt separates receipt/context data from instructions, copies final charged totals and signed adjustments, excludes tender/summary rows, and forbids invented balancing amounts. The schema requires explicit fields with nulls for unknowns and integer quantities. The normalizer rejects non-stop completions, prose quantities and excess decimal precision; avoids the SDN BHD currency trap; filters cash/summary rows; and retains signed rounding. A subsequent local-only cleanup removes purchase categories from negative adjustments and discount scope from positive purchases. Offline replay of all four saved provider responses confirmed unchanged totals, currency and item costs; no further model call was made. This cleanup changes metadata, not the scored financial values.

Measured new model calls

Immutable follow-up requests, raw responses, drafts, usage and reservations record four new model calls, reusing the previously saved PP-OCRv6 OCR text. No completed OCR, Textract or Gemma request was repeated. The production request used temperature zero, strict JSON schema, and 2,048 maximum output tokens. The new runner rejects requests over 32,000 bytes, reserves before every send through the existing ledger, disables SDK retries, and refuses to overwrite reports.

Receipt Exact total Exact complete item-cost multiset Model call + normalization
000 9.00 MYR Yes 1.139 s
001 60.30 MYR Yes, including -5.59 discount and -0.01 rounding 1.564 s
002 33.90 MYR Yes, including -0.02 rounding 2.445 s
003 80.90 MYR Yes, including -0.01 rounding 1.205 s

All four returned valid JSON with normal stop reasons, exact normalized totals, and exact expected complete item-cost lists; none truncated. Median model-stage latency was 1.384 seconds, range 1.139–2.445 seconds. This is a local client calling Bedrock with saved OCR text, not deployed end-to-end Lambda latency. Two responses reported 784 cached prompt tokens each, which may affect latency. These results do not measure uncached throughput or p95.

Currency MYR was supplied as context, so matching it is not independent currency recognition evidence. Merchant labels, categories, translations and quantity accuracy were not scored. In particular, the live output labeled receipt 001's negative discount as DRINK; the later deterministic metadata cleanup removes that category without another model call. Public fixture failures informed the rules; prompt examples use different amounts. This is a development/regression sample, not a held-out set. Switching the model, prompt, schema and normalizer together means the improvement cannot be attributed to model size alone.

The old PP-OCRv6 + Gemma configuration attempted only 000–001: 0/2 exact totals, 0/2 complete item-cost lists, one truncation. The new configuration got 4/4 on both financial metrics, including those two receipts. That supports the new configuration while neither establishing a population accuracy rate nor a like-for-like production speedup.

Packaging and price evidence

The image builds from AWS's Python 3.12 Lambda base and verifies the original model SHA-256 hashes. The actual handler completed an offline container smoke check using mocked S3, a read-only filesystem, no network, 2 GiB memory and one CPU; warm calls reused the model. An initial single-image container check used 419.1 MiB peak RSS and 5.772 seconds for first model load/inference. This is one local container check, not a repeated benchmark or Lambda cold-start measure.

The CDK helper uses 2 GiB x86_64, one OCR inference thread, 60 seconds and ten reserved executions. This architecture matches the evaluated native packages; Graviton remains unmeasured. The 1 GiB ARM Node worker remains separate and bills while waiting for both helper OCR and Bedrock. Step Functions is not implemented by this change. PDFs incur their existing Textract cost.

At standard us-east-1 rates of $0.15/M input and $0.15/M output, the new calls used 4,646 input and 1,055 output tokens: $0.00085515 before cache discounts. That scales to $0.214 per 1,000 model attempts at this sample's usage; it excludes all Lambda, S3, logs and orchestration. The earlier illustrative 1,000-input/1,000-output receipt instead costs $0.30 per 1,000 in model tokens. An assumed two-second 2 GiB x86 OCR invocation adds about $0.067 per 1,000; actual worker waits, cold starts and OCR duration still need measurement.

The ledger retains every original entry unchanged, appending only four ppocr6_ministral_v1 reservations of $0.0055. Cumulative reservations are now $0.077, under the fixed $0.08 cap and strictly below the user's $0.10 limit. Cumulative full-standard-rate service estimates are $0.00500723, not a settled AWS bill. No reservations were released or reset. The retired historical runner now rejects live execution to prevent accidentally replaying old calls with the changed production prompt. No further paid calls were made.

Updated recommendation

Proceed with PP-OCRv6 small + Ministral 3 8B + revised prompting and deterministic validation as the implemented replacement for image receipts. The development sample now extracts all expected financial amounts, with short model responses and far less API cost than keeping Textract for every image. Retain Textract for PDFs. Before production rollout, validate a broader private multilingual, quantity, orientation, large-image and multi-page PDF set and measure actual Lambda cold/warm memory, billed duration and full-pipeline cost. Choose Step Functions only after measuring waiting costs against orchestration charges. Issue #694 remains open for those checks; no production changes were deployed.

PDF follow-up: local rendering and OCR

PDFs now use the same receipt OCR Lambda. PDFium (pypdfium2 5.14.0) rasterizes each visible page and PP-OCRv6 small reads it; Ministral receives the combined text with explicit [Page N of M] boundaries. PDFs with embedded text are also rasterized in this first implementation. There is no embedded-text bypass and no Textract fallback. The former serving Textract adapter, polling loop and IAM grants are removed; its SDK remains development-only for historical evaluation. The worker/helper environment key is now SCAN_OCR_FUNCTION_NAME.

Existing upload restrictions remain 10 MB, one to three pages, unencrypted. The helper independently checks the byte and page bounds and rejects encrypted PDFs even when they open with an empty password. Rendering targets 200 DPI with a longest-edge cap of 2,000 pixels before bitmap allocation. One page, bitmap and decoded image is held at a time and closed before the next page. The model is reused across pages. Aggregate text above 24,000 characters or a failed page rejects the document rather than silently returning a partial document. Blank PDFs return empty text so the existing structurer skips a model call. The container remains 2 GiB x86_64 with one inference thread and a 60-second timeout; the Node client has a 70-second request timeout and disables retries.

Offline native verification

PDF container smoke results come from the built AWS Python 3.12 Lambda image, restricted to one CPU, 2 GiB, a read-only filesystem and no network. The real handler read mocked S3 bodies. The two fixtures are generated by apps/receipt-ocr/smoke_pdf.py, with synthetic English invoice text; no production receipt or private data was used.

Synthetic fixture Pages Expected page order and amount-token presence Local full-document OCR
Digitally generated PDF 3 All three page markers and 10.00 / 20.00 / 30.00 on their respective pages 8.255 s
Scanned-image PDF 3 All three page markers and 10.00 / 20.00 / 30.00 on their respective pages 6.820 s

Peak process RSS across this smoke was 608.8 MiB. The digital fixture ran first and includes lazy model setup; the scanned fixture reused the model. There is one local invocation per fixture. This is functional packaging evidence, not a Lambda cold/warm benchmark, a representative latency distribution, or a comparison between digital and scanned PDF performance. Corrupt PDFs, four-page PDFs and a synthetic empty-user-password encrypted PDF were rejected before OCR; a blank PDF returned empty text. The previous image smoke still passed in the PDF-enabled container.

Standard-library unit tests verify sequential resource cleanup, page limits, encryption rejection, invalid page geometry, aggregate text bounds and failure on unreadable later pages. A mocked service-pipeline test passes a checksum-bound three-page PDF through real upload validation, the Lambda OCR adapter and the Ministral adapter, verifying that every page reaches the extraction request. Infrastructure tests verify that no Textract IAM action remains.

Cost and suitability implications

The serving path no longer pays Textract's $0.0015 per PDF page; rendering and OCR instead incur Lambda compute, with cost increasing with page count and complexity. The Node worker still bills while waiting for OCR and Bedrock. No Lambda was deployed and no PDF was sent to a language model during these checks, so PDF financial extraction accuracy and actual deployed cost remain unmeasured. The earlier four-image Ministral result is not evidence of PDF financial accuracy. Embedded-text bypass, form-field handling, unusual layouts, small print and a representative private PDF set remain evaluation work.

No additional paid test ran. The ledger is byte-for-byte unchanged at $0.077 reserved (SHA-256 e611a1e00faca4af1a175b0461fb8a82a7377ba803f28b15224831a2b86eb61f), with every prior reservation retained. Nothing was deployed. The implemented image/PDF path is ready for code review; production rollout should follow the broader accuracy and real-Lambda checks already tracked in issue #694.

Production follow-up — reviewed 24 October 2026

PR #742 reached production through the successful Main Pipeline. The following observations come from two existing user submissions on 22 October, not additional paid evaluation calls. Aggregate evidence is in production-reports-2026-10-22.json. Private uploads, OCR text, account identifiers and payment details are excluded.

Existing submission OCR duration / billed duration OCR peak memory Node worker duration / billed duration Worker peak memory
First 39.902 s / 40.365 s 696 MiB of 2,048 MiB 43.246 s / 44.949 s 225 MiB of 1,024 MiB
Second 32.185 s / 32.390 s 651 MiB of 2,048 MiB 34.501 s / 36.137 s 224 MiB of 1,024 MiB

Both use fresh execution environments. Python init alone was 462/204 ms; lazy OCR/model initialization runs inside the invocation and is included in duration. These two different receipts do not establish warm latency, tail latency, concurrency throughput, or the reason for the cold-time slowdown. Local offline OCR of the same uploads took 8.135 s for the first and 4.004 s for the second in one container, with the latter reusing the model. That is functional verification with no AWS/model calls, not a Lambda replacement benchmark. Local timings substantially underestimated these deployed durations.

At published us-east-1 first-tier on-demand rates, the two OCR invocations imply $0.002425 compute and the waiting ARM Node workers $0.001081 compute; four requests add $0.0000008. Lambda alone is approximately $0.003507 for the pair, or $1.75 per 1,000 attempts if these two cold cases were representative. This uses observed billed duration with standard prices, not an AWS invoice. It excludes Bedrock, storage, logs, DynamoDB, free tiers and discounts. The OCR helper alone is about $1.21/1,000; standalone Textract OCR is $1.50/1,000. An end-to-end saving over the old pipeline is not established by these two cases, and earlier local-time savings scenarios must not be treated as measured Lambda economics.

The worker cost averages about $0.00054 per receipt, much of it waiting. This strengthens the case for testing direct-service Step Functions orchestration: ten Standard transitions at $0.025/1,000 would cost $0.00025 per receipt before validation/persistence compute and other states. Actual savings depend on the final state count and remaining work. Step Functions would not remove the 2 GiB OCR function's own 32–40 seconds. It remains a separate implementation and cost evaluation; no workflow was deployed or executed for this follow-up. Also profile a 512 MiB Node worker before adding orchestration: halving memory would halve its waiting allocation at unchanged duration, but these two image peaks do not establish safe PDF/worst-case headroom or the CPU/latency effect. No function memory setting changed in this follow-up.

The second receipt has two purchases of 4.49 and 2.29, a total of 6.78 marked as including VAT, and a tax-table disclosure of 1.13. Its reported item excess of 1.13 is consistent with counting that disclosure again. The expired saved model response cannot establish its exact raw item labels. The correction adds explicit included/excluded-tax examples and a deterministic guard: remove positive tax-only rows only with explicit included-tax OCR evidence and known non-tax amounts that already equal the grand total. Keep additional taxes, refunds, unknown amounts and unexplained mismatches; never invent a balancing row. Mocked regression tests exercise this without buying another completion.

No worker or model-fallback error appeared in the inspected submission window; both workers returned normally. The first client error's wording is unavailable, so its exact cause remains unresolved. A bounded transport retry now preserves the same attempt ID and request, allowing recovery of a lost acceptance response without a new credit reservation or paid dispatch. API rejections and failed jobs are not automatically restarted. Opaque client-operation and server job-outcome events improve correlation on any recurrence. This recovery is not proof of the original failure's cause.

Recommendation: ship the conservative VAT and client/UI fixes with regression checks, retain the user-visible total mismatch warning, and prioritize cold/warm Lambda profiling and a held-out receipt set before claiming broad accuracy or large cost savings. Evaluate Step Functions using these observed waiting costs. The persistent evaluation ledger remains byte-for-byte unchanged at $0.077 reserved under its $0.08 cap; no paid inference was repeated or added here.

View Markdown source