sharp: AWS Lambda running out of memory pretty quickly - then crash/timeout

I am using sharp in one of my application which runs on AWS lambda. I am not using any fancy operation with sharp - just resize & toFormat.

Now the problem is - Lambda “Max Memory Used:” is keep on increasing until it hits the roof and then crash. To illustrate the problem, i have written a small function below

"use strict";

process.env.VIPS_DISC_THRESHOLD = '20m';

const v8    = require("v8"),
    log     = console.log,
    Busboy  = require('busboy'),
    sharp   = require("sharp");

v8.setFlagsFromString('--max_old_space_size=50');  
//v8.setFlagsFromString('--max_new_space_size=50'); 
v8.setFlagsFromString('--trace_fragmentation');
//v8.setFlagsFromString('--trace_fragmentation_verbose');
v8.setFlagsFromString('--expose_gc');  
sharp.cache(false);
sharp.concurrency(1);

module.exports.handler = (event) => new Promise((resolve, reject) => {
    let buffer;
    const busboy = new Busboy({
        headers: {
            'content-type': event.headers['content-type'] || event.headers['Content-Type'],
        }
    });

    busboy
    .on('file', (fieldname, file, filename, encoding, mimetype) => {
        log(fieldname, mimetype, encoding, filename);
        file.on('data', (data) => buffer = data);
    })
    .on('error', (err) => reject(`error: ${err}`))
    .on('finish', () => {
        sharp(buffer)
        .sequentialRead(true)
        //.resize({width: 500, height: 500})   //this operation can be anything - toFormat(), webp() etc.
        .toFormat("webp", {force: true})       //this operation can be anything - resize(), webp() etc.
        .toBuffer()
        .then(() => {
            log("=> before gc memoryUsage", process.memoryUsage());
            global.gc();
            log("=> after gc memoryUsage ", process.memoryUsage());

            const headers = {
                'content-type': "text/plain; charset=us-ascii"
            };
            resolve({statusCode: 200, body: "done", isBase64Encoded: false, headers});
        })
        .catch((err) => {
            log(err);
            reject(`error: ${err}`);
        });
    });

    busboy.write(Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8'));
    busboy.end();
});

Now, i could see these memory related question in the forum and i tried to incorporate all the suggestions i could find but without any luck

things tried

process.env.VIPS_DISC_THRESHOLD = '20m';
v8.setFlagsFromString('--max_old_space_size=50');
v8.setFlagsFromString('--max_new_space_size=50');  //this is not supported any more
sharp.cache(false);
sharp.concurrency(1);
sequentialRead(true)

also enabled gc - v8.setFlagsFromString(‘–expose_gc’); - and started running GC just before i return from the handler.

I am printing memoryUsage before and after the GC call on every function invokation. the heap usage is almost constant as expected - only RSS keep on increasing and finally “Max Memory Used” hits the root.

i tried with different “MemorySize” starting from 128M to 512M (even though my sample app is very small, i tried to increase the MemorySize to prove my point)

Few sample logs below in the start the Log is something like below - Max Memory Used: 162 MB

imageFile image/jpeg 7bit 00 (3rd copy).JPG before gc memoryUsage { rss: 82427904, heapTotal: 22519808, heapUsed: 13582184, external: 1467043 } compaction-selection: space=OLD_SPACE reduce_memory=0 pages=0 total_live_bytes=70 space=CODE_SPACE reduce_memory=0 pages=0 total_live_bytes=0 2 pages, 580344 (56.3%) free after gc memoryUsage { rss: 81100800, heapTotal: 20082688, heapUsed: 9323088, external: 1467043 } Duration: 3788.14 ms Billed Duration: 3800 ms Memory Size: 512 MB Max Memory Used: 162 MB

After few itertaion the Log is Max Memory Used: 369 MB

imageFile image/jpeg 7bit ali-al-mufti-365944-unsplash.jpg before gc memoryUsage { rss: 195997696, heapTotal: 16015360, heapUsed: 13140688, external: 3533290 } compaction-selection: space=OLD_SPACE reduce_memory=0 pages=0 total_live_bytes=21 compaction-selection: space=CODE_SPACE reduce_memory=0 pages=0 total_live_bytes=0 2 pages, 680840 (66.0%) free after gc memoryUsage { rss: 191299584, heapTotal: 11300864, heapUsed: 8301824, external: 3533258 } Billed Duration: 4100 ms Memory Size: 512 MB Max Memory Used: 369 MB

Towards the end before memory running out - Max Memory Used: 464 MB

compaction-selection: space=OLD_SPACE reduce_memory=0 pages=0 total_live_bytes=0 compaction-selection: space=CODE_SPACE reduce_memory=0 pages=0 total_live_bytes=0 [MAP_SPACE]: 2 pages, 692896 (67.2%) free imageFile image/jpeg 7bit florian-klauer-489-unsplash.jpg before gc memoryUsage { rss: 243482624, heapTotal: 23261184, heapUsed: 19224656, external: 5971426 } compaction-selection: space=OLD_SPACE reduce_memory=0 pages=0 total_live_bytes=0 compaction-selection: space=CODE_SPACE reduce_memory=0 pages=0 total_live_bytes=0 2 pages, 686384 (66.6%) free after gc memoryUsage { rss: 237264896, heapTotal: 15994880, heapUsed: 12989440, external: 4670102 } Billed Duration: 19900 ms Memory Size: 512 MB Max Memory Used: 464 MB

Thanks

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 30 (10 by maintainers)

Most upvoted comments

I’ve been using sharp with Lambda in production environment over 1+ years. (Thanks to @lovell 🙇)

This issue is related with Lambda runtime’s behavior. As you may know, Lambda can reuse container, and freezes container until after invocation. So the chance of V8’s GC execution is quite low. also there is no background task capabilities including GC. GC only can be executed during Lambda invocation.

so IMHO It’s better to use larger memory size configuration, Especially if you have to handle super high resolution images. Lambda allocates CPU power linearly in proportion to the amount of memory configured. there won’t be big cost difference - allocating 2x memory size will give 2x processing speed. also it gives better function stability since it has more memory size - New container will be used before reaching max memory.

Also, Increasing VIPS_DISC_THRESHOLD might be help if you are having memory issues. (Please refer to #707 or http://jcupitt.github.io/libvips/API/current/VipsImage.html#VIPS-ACCESS-RANDOM:CAPS)

To fix the issue I just increased the amount of memory for the Lambda function to over 1GB. Eventually the garbage collector will kick in and the function will level off around 800-900mb of usage.

I ran in to a similar problem, and after digging, I think I understand why increasing the memory for the Lambda is a solution.

NodeJS runs on top of the V8 engine. V8 has a setting --max-old-space-size which has a default value of 512MB. This value represents the tipping point after which V8 will start to spend more time on garbage collection in order to free up memory.

If your Lambda has less than 512MB of memory, you will likely see that memory fill up before the garbage collection bothers to clean it up.

AFAIK there is no way to update the Node/V8 runtime (Within the AWS Lambda) and adjust that value. Or even to expose the gc (using --expose-gc) for and manually run GC for your lambda

@mckalexee Glad you worked it out. Maximising CPU share via the “memory” setting remains the recommend approach for using sharp on AWS Lambda.

https://sharp.pixelplumbing.com/install#aws-lambda

I was able to figure out the issue, but I don’t think the answer is going to be satisfying to everyone.

For context I’m running an image resizer & converter that uses sharp on Node 12.x in Lambda. We’re doing about 4k invocations an hour.

To fix the issue I just increased the amount of memory for the Lambda function to over 1GB. Eventually the garbage collector will kick in and the function will level off around 800-900mb of usage.

Since you get more CPU with larger memory sizes in Lambda, it actually doesn’t seem to increase the cost in the long run. Doubling the amount of memory decreased the runtime by about half.

Currently I have the function set to 2GB of memory, and I haven’t had any memory issues.

EDIT:

I see that I just repeated @mooyoul, but the advice is still sound.

@rijkvanzanten Progressive/interlaced JPEGs have to be fully-loaded into memory, so width * height * channels provides a quick estimate. libjpeg-turbo provides pixel data as RGB so in this example 7500 * 11000 * 3 = 247500000 bytes. Non-interlaced JPEG and other formats are harder to estimate, so you’ll need to experiment.