Gravid Banner

AWS Lambda function billed for longer than execution time

Over the last few weeks I have been updating a number of old AWS Lambda functions to bring them up to date with Node 22. In doing so I came across a curious anomaly in one of my new functions. Under most use cases the Lambda took less than two seconds to return a value. In one particular case, however, it was consistently taking over 70 seconds. This is far too long for an API call (it would lose me users), and could also have a significant impact on the amount being billed.

I added a number of console.log statements to debug the Lambda and I found another intriguing fact. The difference between the first timestamp and the last one (immediately after the callback function) was less than two seconds. And yet the billed time was still over seventy seconds.

A google search revealed a similar question on StackOverflow. AWS Lambda, billed time longer than processing time

The solution proposed here is to add the following line of code

context.callbackWaitsForEmptyEventLoop = false;
JavaScript

Sure enough, as soon as I popped this into my code, the Lambda finished in just under two seconds. The question in my mind remains as to why. As the StackOverflow says the Lambda function is waiting for the event loop to be empty before returning.

By commenting out all of my internal calls and gradually re-including them, I tracked it down. One of the API calls I was consuming returned a 302 code. Because of thus I had updated my https request to get the redirect URL out of the header as follows:

  const httpsRequest = async(host, path, key) =>{
    var data = "";

    await new Promise((resolve, reject) =>{
      var options =  {
        hostname: host,
        path: path
      };

      if (key != "" ) options.headers = {"Authorization": key} 

      https.get(options, (r) => {
        if (r.statusCode == 302) {
          data = r.headers.location;
          resolve();
        }
        else if (r.statusCode != 200) {
          retVal = "Error " + r.statusCode;
          resolve();
        }
        else {
          r.setEncoding('utf8');
          r.on('data', (chunk) => { data += chunk; });
          r.on('end', () => { resolve(); });
        }
    }).on('error', (e) => {
        data = "HTTPS Error";
        reject();
      });
    });

    return data;
  }
JavaScript

This code gets me all that I need – as the only useful piece of data is the redirect url contained in r.headers.location. The problem is, as I now discovered, it leaves the unprocessed body of the http response sitting in the event queue.

The solution I opted for in this case was to always process the response body.

  const httpsRequest = async(host, path, key) =>{
    var data = "";

    await new Promise((resolve, reject) =>{
      var options =  {
        hostname: host,
        path: path
      };

      if (key != "" ) options.headers = {"Authorization": key} 

      https.get(options, (r) => {
        r.setEncoding('utf8');
        r.on('data', (chunk) => { data += chunk; });
        r.on('end', () => { 
          if (r.statusCode == 302) data = r.headers.location;
          else if (r.statusCode != 200) data = "Error " + r.statusCode;
          resolve();
        });
      }).on('error', (e) => {
        data = "HTTPS Error";
        reject();
      });
    });

    return data;
  }
JavaScript

With this in place, my code completed normally in under two seconds without the need to set context.callbackWaitsForEmptyEventLoop


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *