- Small: OpenCV and its codecs are linked statically into a single stripped
cv2.abi3.so, without FFmpeg or OpenBLAS. - Fast cold start: Precompiled bytecode is included, because Lambda cannot write a bytecode cache to
/opt. - Runtime-matched: Built inside the official
public.ecr.aws/lambda/pythonimages, and every zip is invoked through the Lambda Runtime Interface Emulator before release. - Verifiable: Pinned sources and build tools, deterministic zips,
SHA256SUMS, a CycloneDX SBOM per zip, and GitHub build provenance attestations.
1. Download a zip from the latest release. The asset URL is stable across releases.
curl -LO https://github.com/076923/aws-lambda-python-opencv-layer/releases/latest/download/opencv-4.14.0-minimal-python3.13-arm64.zip2. Publish it as a layer. The runtime and architecture must match your function.
aws lambda publish-layer-version \
--layer-name opencv-minimal \
--zip-file fileb://opencv-4.14.0-minimal-python3.13-arm64.zip \
--compatible-runtimes python3.13 \
--compatible-architectures arm64Note
Every zip is under the 50 MB limit for direct upload. If your own build is larger, upload it to S3 first and pass --content S3Bucket=<bucket>,S3Key=<key>.
3. Use it in your function.
import base64
import cv2
import numpy as np
def handler(event, context):
data = np.frombuffer(base64.b64decode(event["image"]), np.uint8)
img = cv2.imdecode(data, cv2.IMREAD_COLOR)
thumb = cv2.resize(img, (256, 256), interpolation=cv2.INTER_AREA)
ok, buf = cv2.imencode(".webp", thumb, [cv2.IMWRITE_WEBP_QUALITY, 80])
return {"image": base64.b64encode(buf).decode()}Asset name: opencv-{opencv}-{profile}-python{python}-{arch}.zip
| Option | Values |
|---|---|
opencv |
4.14.0, 5.1.0 |
profile |
minimal, standard, dnn |
python |
3.12, 3.13, 3.14 |
arch |
x86_64, arm64 |
| Profile | Modules | Use for |
|---|---|---|
minimal |
core, imgproc, imgcodecs | Resize, crop, color conversion, filters, encode and decode |
standard |
minimal + features2d, calib3d, flann, objdetect, photo, video, ml | Feature matching, homography, QR codes, inpainting, optical flow |
dnn |
standard + dnn | Running ONNX and other models with cv2.dnn |
Sizes for Python 3.13 with NumPy 2.5.4 (zipped / unzipped, MB). Other Python versions differ by less than 0.3 MB.
| Profile | 4.14.0 x86_64 | 4.14.0 arm64 | 5.1.0 x86_64 | 5.1.0 arm64 |
|---|---|---|---|---|
minimal |
12.4 / 33.6 | 10.5 / 29.2 | 14.6 / 39.3 | 13.0 / 35.1 |
standard |
15.7 / 41.7 | 13.4 / 36.0 | 17.0 / 44.9 | 15.0 / 39.8 |
dnn |
18.3 / 48.6 | 15.7 / 42.0 | 20.9 / 57.0 | 18.4 / 50.1 |
For comparison, the official opencv-python-headless 4.14.0.94 wheel with NumPy is about 72 MB zipped and 215 MB unzipped. Each release lists the size of every asset.
Tip
arm64 (Graviton) is about 20% cheaper per GB-second than x86_64.
Note
OpenCV 5 renamed some modules (features2d → features, calib3d → calib, geometry, stereo). The standard profile includes the equivalent modules for each version.
The layers keep only what image processing on Lambda needs.
| Not included | Impact |
|---|---|
FFmpeg, GStreamer, videoio, highgui |
cv2.VideoCapture, cv2.VideoWriter, and cv2.imshow are not available |
| TIFF, JPEG 2000, OpenEXR, AVIF, HDR, PNM codecs | Only JPEG, PNG, and WebP can be read and written |
| OpenBLAS (OpenCV and NumPy) | NumPy uses its bundled lapack_lite. Large matmul and linalg calls are slower |
| IPP, OpenCL, TBB, Eigen | OpenCV uses its own SIMD kernels and pthreads |
| Haar cascade XML files | Ship the XML files you need with your function. cv2.CascadeClassifier is only available with 4.14.0, because OpenCV 5 removed it from the main repository |
python3.11 and older |
These runtimes run on Amazon Linux 2 and are not supported |
Warning
NumPy is built without BLAS. This is fine for image processing, where NumPy mostly holds pixel arrays for OpenCV. If your function uses NumPy as a compute engine (large matrix products, decompositions, solvers), these layers are the wrong choice. Use the official opencv-python-headless and numpy wheels, which bundle OpenBLAS.
Warning
x86_64 binaries require AVX2. cv2 is compiled with CPU_BASELINE=AVX2 and has no fallback path. Every AWS Lambda x86_64 host supports AVX2, but other machines may not, such as older CPUs or some emulators. On a CPU without AVX2, import cv2 stops with an error that the build does not support the current CPU. The arm64 builds use the default NEON baseline.
| Technique | Effect |
|---|---|
Static build into one cv2.abi3.so |
No bundled .libs directories or duplicate runtimes |
CPU_BASELINE=AVX2 on x86_64 without runtime dispatch |
No SSE4, AVX, or AVX-512 copies of the kernels. Every Lambda x86_64 host supports AVX2 |
-ffunction-sections, --gc-sections, strip |
Removes unused code and symbols |
| NumPy without BLAS | Removes about 27 MB of OpenBLAS and gfortran runtime |
Remove NumPy tests, headers, static libs, f2py, .pyi stubs |
Removes files that are only needed for development |
.pyc compiled with unchecked-hash |
Zip rounds file times to 2 seconds, so timestamp-checked bytecode would be ignored on every cold start |
Every zip goes through two checks before it can be merged or released. CI fails if either one fails.
| Check | Where | What it verifies |
|---|---|---|
| Smoke test | Build container, before zipping | cv2 and numpy import, and basic image operations work |
| Lambda invocation | Fresh public.ecr.aws/lambda/python container with the zip extracted to a read-only /opt, invoked through the Runtime Interface Emulator with 1024 MB of memory |
See below |
The Lambda invocation runs tests/lambda/handler.py twice, once cold and once warm, and checks:
cv2andnumpyload from/opt/python, and the precompiled bytecode is present- JPEG, PNG, and WebP round trips
- ORB matching recovers a known shift through
findHomography(standard,dnn) cv2.dnn.blobFromImage(dnn)- Each profile has its modules and lacks the ones it excludes, such as
cv2.dnninstandardandcv2.VideoCaptureeverywhere - Import time, a 12 MP decode → resize → blur → WebP pipeline, and peak memory, which are recorded in each zip's
.json
Note
The emulator runs on GitHub-hosted runners, so the timings are indicative only. Cold start, memory, and latency on AWS Lambda itself are not measured.
To run the Lambda invocation locally after a build, run python3 scripts/test_layer.py dist.
| Item | How |
|---|---|
| Source archives | The OpenCV and NumPy source archives are verified against SHA256 hashes pinned in scripts/build_layer.py before building |
| Build tools | CMake, Ninja, Meson, meson-python, and Cython versions are pinned. NumPy builds without build isolation, so only the pinned tools are used |
| Build environment | The Lambda image digest, GCC version, and tool versions are recorded in each zip's .json |
| GitHub Actions | Actions are pinned to commit SHAs and updated by Dependabot |
| SBOM | Each release includes a CycloneDX SBOM (.cdx.json) per zip, listing OpenCV, NumPy, and the third-party libraries linked into cv2 |
| Provenance | Each zip has a GitHub build provenance attestation |
SHA256SUMS proves that a download is intact. The attestation also proves that the zip was built by this repository's workflow:
gh attestation verify opencv-4.14.0-minimal-python3.13-arm64.zip -R 076923/aws-lambda-python-opencv-layerRequires Docker. Building arm64 on an x86_64 machine uses QEMU emulation and is slow.
# ./build.sh <python> <arch> all <opencv> <profile> [--numpy <version>]
./build.sh 3.13 arm64 all 4.14.0 minimaldist/opencv-4.14.0-minimal-python3.13-arm64.zip
dist/opencv-4.14.0-minimal-python3.13-arm64.json # versions, sizes, build info, test results
dist/opencv-4.14.0-minimal-python3.13-arm64.cdx.json # CycloneDX SBOM
python3 scripts/test_layer.py dist # invoke every zip through the Lambda emulatorBuild stages
The build has three stages, which CI runs as separate jobs.
| Stage | Runs once per | Output |
|---|---|---|
opencv |
OpenCV × profile × arch | cv2 built with the Python Limited API, so one binary serves Python 3.12 and later |
numpy |
Python × arch | NumPy without BLAS |
package |
Python × arch | Every zip for that runtime, after a smoke test |
./build.sh 3.12 x86_64 opencv 4.14.0 standard # artifacts/opencv-4.14.0-standard-x86_64.tar.gz
./build.sh 3.13 x86_64 numpy # artifacts/numpy-2.5.4-python3.13-x86_64.tar.gz
./build.sh 3.13 x86_64 package # dist/*.zip for every built cv2Layer structure
Lambda extracts the layer to /opt, and /opt/python is on sys.path.
opencv-{opencv}-{profile}-python{python}-{arch}.zip
└── python/
├── cv2/
│ ├── __init__.py
│ ├── python-3/cv2.abi3.so
│ └── licenses/
├── numpy/
└── numpy-{version}.dist-info/
Push a v* tag. GitHub Actions builds and tests all 36 zips, then attaches them to the release with their SBOMs, SHA256SUMS, provenance attestations, and a size table.
To change the OpenCV or NumPy version, update the workflow matrix and add the SHA256 of the new source archive to OPENCV_SOURCES or NUMPY_SOURCES in scripts/build_layer.py.
git tag v2026.11.0
git push origin v2026.11.0Daehee Yun ([email protected])