From 182a48738327006166fdb8b47888f4ac52ac14e1 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sat, 26 Sep 2026 23:55:34 +0200 Subject: [PATCH 1/2] Fail fast when a package exceeds the AWS Lambda size limit AWS Lambda rejects deployment packages larger than 250 MB once unzipped. osls zipped and uploaded such packages anyway, and the deployment only failed minutes later, when CloudFormation created the function: "Unzipped size must be smaller than 262144000 bytes". Add up the size of the files before zipping them and fail right away, listing the largest top-level files and directories of the package and pointing to `package.patterns`. This applies to service, function and layer packages, for both `package` and `deploy`. The check is skipped when a plugin overrides getFileContent() or getFileContentAndStat(), as the zipped content can then differ from the files on disk. Layers also count toward the limit, but the unzipped size of external layers is not known locally: only packages that are too large on their own are rejected. Claude-Session: https://claude.ai/code/session_01Awk33iZ9mDSg1xgSAPkgGK --- docs/guides/packaging.md | 4 ++ lib/plugins/package/lib/zip-service.js | 47 +++++++++++++++++++ .../plugins/package/lib/zip-service.test.js | 41 ++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/docs/guides/packaging.md b/docs/guides/packaging.md index 6f4d8efe3a..5e7fa0e25d 100644 --- a/docs/guides/packaging.md +++ b/docs/guides/packaging.md @@ -79,6 +79,10 @@ package: - '!.git/**' ``` +### Size limit + +AWS Lambda limits a function's code and layers to 250 MB once unzipped. When a package built by osls is larger than that, `osls package` and `osls deploy` fail before anything is uploaded, and list the largest files and directories of the package. Use `patterns` to exclude files that are not needed at runtime. + ### Artifact For complete control over the packaging process you can specify your own artifact zip file. diff --git a/lib/plugins/package/lib/zip-service.js b/lib/plugins/package/lib/zip-service.js index 711ef3a817..02838fc170 100644 --- a/lib/plugins/package/lib/zip-service.js +++ b/lib/plugins/package/lib/zip-service.js @@ -55,6 +55,10 @@ const getArchiveEntryName = (filePath, prefix) => { const zipMtime = new Date(0); const maxConcurrentFileStats = 64; +// AWS Lambda rejects packages larger than 250 MB (262144000 bytes) once unzipped +const maxUnzippedSize = 250 * 1024 * 1024; +const packagingDocsUrl = + 'https://github.com/oss-serverless/osls/blob/4.x/docs/guides/packaging.md#patterns'; const getFileMode = (stat) => Number(stat.mode) & 0o100 || process.platform === 'win32' ? 0o100755 : 0o100644; @@ -100,6 +104,43 @@ const assertSameFile = (entry, currentStat) => { } }; +// Lambda counts MB as 1,024 KB +const formatMegabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`; + +// Fail before zipping instead of letting the deployment fail minutes later in CloudFormation +const assertUnzippedSizeWithinLimit = (entries, zipFileName) => { + const totalSize = entries.reduce((total, entry) => total + Number(entry.stat.size), 0); + if (totalSize < maxUnzippedSize) return; + + const sizeByTopLevelPath = new Map(); + for (const entry of entries) { + const separatorIndex = entry.name.indexOf('/'); + const topLevelPath = + separatorIndex === -1 ? entry.name : entry.name.slice(0, separatorIndex + 1); + sizeByTopLevelPath.set( + topLevelPath, + (sizeByTopLevelPath.get(topLevelPath) || 0) + Number(entry.stat.size) + ); + } + const largestPaths = Array.from(sizeByTopLevelPath) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + const pathColumnWidth = Math.max(...largestPaths.map(([topLevelPath]) => topLevelPath.length)); + + throw new ServerlessError( + [ + `The "${zipFileName}" package is ${formatMegabytes(totalSize)} unzipped, above the 250 MB AWS Lambda limit (function code and layers combined).`, + 'Largest files and directories in the package:', + ...largestPaths.map( + ([topLevelPath, size]) => + ` ${topLevelPath.padEnd(pathColumnWidth)} ${formatMegabytes(size).padStart(9)}` + ), + `Exclude files that are not needed at runtime with "package.patterns": ${packagingDocsUrl}`, + ].join('\n'), + 'PACKAGE_TOO_LARGE' + ); +}; + const compareUtf8 = (left, right) => Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')); @@ -319,6 +360,12 @@ const zipService = { entries.sort(compareZipEntries); + // A getFileContent() override can change file sizes, the size on disk is only + // known to be the unzipped size when files are zipped as they are + if (usesDefaultGetFileContent) { + assertUnzippedSizeWithinLimit(entries, zipFileName); + } + for (const entry of entries) { const options = { mode: entry.mode, diff --git a/test/unit/lib/plugins/package/lib/zip-service.test.js b/test/unit/lib/plugins/package/lib/zip-service.test.js index 96b3bec376..334d464da0 100644 --- a/test/unit/lib/plugins/package/lib/zip-service.test.js +++ b/test/unit/lib/plugins/package/lib/zip-service.test.js @@ -1406,7 +1406,8 @@ describe('zipService', () => { serverless.utils.writeFileDir(largeFilePath); await fs.promises.writeFile(largeFilePath, ''); - await fs.promises.truncate(largeFilePath, 512 * 1024 * 1024); + // Just below the 250 MB AWS Lambda limit, larger packages are rejected + await fs.promises.truncate(largeFilePath, 240 * 1024 * 1024); await packagePlugin.zipFiles(['event.json'], getTestArtifactFileName('warmup')); forceGc(); @@ -1425,8 +1426,8 @@ describe('zipService', () => { } // The sampled peak includes dead stream chunks that V8 has not collected yet, - // and that GC lag alone can exceed 64 MiB. The cap only needs to stay far - // below the 512 MiB file size to prove the file was streamed, not materialized. + // and that GC lag alone can exceed 64 MiB. The cap only needs to stay well + // below the 240 MiB file size to prove the file was streamed, not materialized. expect(peakArrayBuffers - before.arrayBuffers).to.be.lessThan(128 * 1024 * 1024); forceGc(); @@ -1444,6 +1445,40 @@ describe('zipService', () => { 'No files to package' )); + it('rejects packages above the AWS Lambda unzipped size limit before zipping them', async () => { + serverless.utils.writeFileSync(path.join(tmpDirPath, 'handler.js'), 'handler'); + // Sparse files: only their size is read + const largeFiles = { + 'vendor/large.bin': 200 * 1024 * 1024, + 'node_modules/medium.bin': 60 * 1024 * 1024, + }; + for (const [filePath, size] of Object.entries(largeFiles)) { + serverless.utils.writeFileSync(path.join(tmpDirPath, filePath), ''); + await fs.promises.truncate(path.join(tmpDirPath, filePath), size); + } + const artifactFilePath = path.join(serverless.serviceDir, '.serverless', 'too-large.zip'); + + let error; + try { + await packagePlugin.zipFiles(['handler.js', ...Object.keys(largeFiles)], 'too-large.zip'); + } catch (caughtError) { + error = caughtError; + } + + expect(error).to.have.property('code', 'PACKAGE_TOO_LARGE'); + expect(error.message).to.equal( + [ + 'The "too-large.zip" package is 260.0 MB unzipped, above the 250 MB AWS Lambda limit (function code and layers combined).', + 'Largest files and directories in the package:', + ' vendor/ 200.0 MB', + ' node_modules/ 60.0 MB', + ' handler.js 0.0 MB', + 'Exclude files that are not needed at runtime with "package.patterns": https://github.com/oss-serverless/osls/blob/4.x/docs/guides/packaging.md#patterns', + ].join('\n') + ); + expect(fs.existsSync(artifactFilePath)).to.equal(false); + }); + it('configures metadata stat concurrency with ext/promise/limit', () => { let configuredLimit; const fakeLimit = function (limitValue, callback) { From 6c2d164d39aa2a70a6a69e94226e8237c6348349 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 25 Oct 2026 12:21:23 +0000 Subject: [PATCH 2/2] Report package sizes in MiB --- docs/guides/packaging.md | 2 +- lib/plugins/package/lib/zip-service.js | 13 +++++-------- .../lib/plugins/package/lib/zip-service.test.js | 10 +++++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/guides/packaging.md b/docs/guides/packaging.md index 5e7fa0e25d..6f869a0dc0 100644 --- a/docs/guides/packaging.md +++ b/docs/guides/packaging.md @@ -81,7 +81,7 @@ package: ### Size limit -AWS Lambda limits a function's code and layers to 250 MB once unzipped. When a package built by osls is larger than that, `osls package` and `osls deploy` fail before anything is uploaded, and list the largest files and directories of the package. Use `patterns` to exclude files that are not needed at runtime. +AWS Lambda limits a function's code and layers to 250 MiB once unzipped. When a package built by osls is larger than that, `osls package` and `osls deploy` fail before anything is uploaded, and list the largest files and directories of the package. Use `patterns` to exclude files that are not needed at runtime. ### Artifact diff --git a/lib/plugins/package/lib/zip-service.js b/lib/plugins/package/lib/zip-service.js index 02838fc170..224289c2e6 100644 --- a/lib/plugins/package/lib/zip-service.js +++ b/lib/plugins/package/lib/zip-service.js @@ -55,7 +55,7 @@ const getArchiveEntryName = (filePath, prefix) => { const zipMtime = new Date(0); const maxConcurrentFileStats = 64; -// AWS Lambda rejects packages larger than 250 MB (262144000 bytes) once unzipped +// AWS Lambda rejects packages of 250 MiB (262144000 bytes) or more once unzipped const maxUnzippedSize = 250 * 1024 * 1024; const packagingDocsUrl = 'https://github.com/oss-serverless/osls/blob/4.x/docs/guides/packaging.md#patterns'; @@ -104,10 +104,8 @@ const assertSameFile = (entry, currentStat) => { } }; -// Lambda counts MB as 1,024 KB -const formatMegabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`; +const formatMebibytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MiB`; -// Fail before zipping instead of letting the deployment fail minutes later in CloudFormation const assertUnzippedSizeWithinLimit = (entries, zipFileName) => { const totalSize = entries.reduce((total, entry) => total + Number(entry.stat.size), 0); if (totalSize < maxUnzippedSize) return; @@ -129,11 +127,11 @@ const assertUnzippedSizeWithinLimit = (entries, zipFileName) => { throw new ServerlessError( [ - `The "${zipFileName}" package is ${formatMegabytes(totalSize)} unzipped, above the 250 MB AWS Lambda limit (function code and layers combined).`, + `The "${zipFileName}" package is ${formatMebibytes(totalSize)} unzipped, above the 250 MiB AWS Lambda limit (function code and layers combined).`, 'Largest files and directories in the package:', ...largestPaths.map( ([topLevelPath, size]) => - ` ${topLevelPath.padEnd(pathColumnWidth)} ${formatMegabytes(size).padStart(9)}` + ` ${topLevelPath.padEnd(pathColumnWidth)} ${formatMebibytes(size).padStart(10)}` ), `Exclude files that are not needed at runtime with "package.patterns": ${packagingDocsUrl}`, ].join('\n'), @@ -360,8 +358,7 @@ const zipService = { entries.sort(compareZipEntries); - // A getFileContent() override can change file sizes, the size on disk is only - // known to be the unzipped size when files are zipped as they are + // A getFileContent() override can change the unzipped size if (usesDefaultGetFileContent) { assertUnzippedSizeWithinLimit(entries, zipFileName); } diff --git a/test/unit/lib/plugins/package/lib/zip-service.test.js b/test/unit/lib/plugins/package/lib/zip-service.test.js index 334d464da0..3c2ce708b7 100644 --- a/test/unit/lib/plugins/package/lib/zip-service.test.js +++ b/test/unit/lib/plugins/package/lib/zip-service.test.js @@ -1406,7 +1406,7 @@ describe('zipService', () => { serverless.utils.writeFileDir(largeFilePath); await fs.promises.writeFile(largeFilePath, ''); - // Just below the 250 MB AWS Lambda limit, larger packages are rejected + // Just below the 250 MiB AWS Lambda limit, larger packages are rejected await fs.promises.truncate(largeFilePath, 240 * 1024 * 1024); await packagePlugin.zipFiles(['event.json'], getTestArtifactFileName('warmup')); @@ -1468,11 +1468,11 @@ describe('zipService', () => { expect(error).to.have.property('code', 'PACKAGE_TOO_LARGE'); expect(error.message).to.equal( [ - 'The "too-large.zip" package is 260.0 MB unzipped, above the 250 MB AWS Lambda limit (function code and layers combined).', + 'The "too-large.zip" package is 260.0 MiB unzipped, above the 250 MiB AWS Lambda limit (function code and layers combined).', 'Largest files and directories in the package:', - ' vendor/ 200.0 MB', - ' node_modules/ 60.0 MB', - ' handler.js 0.0 MB', + ' vendor/ 200.0 MiB', + ' node_modules/ 60.0 MiB', + ' handler.js 0.0 MiB', 'Exclude files that are not needed at runtime with "package.patterns": https://github.com/oss-serverless/osls/blob/4.x/docs/guides/packaging.md#patterns', ].join('\n') );