Joel Haubold in aws 2 minutes to read

Stateless Doesn't Mean No State!

As you know, Lambda functions run in a contained environment. But, what you may not know is that this environment is also often reused for subsequent invocations.

To demonstrate this, we created a Node.js Lambda function with the following code and ran it multiple times. We encourage you to follow along and do the same.

var someGlobal;

exports.handler = (event, context, callback) => {
    if (someGlobal) {
        console.log('Global variable:', someGlobal);
    } else {
        console.log('Global variable not set, setting it now');
        someGlobal = 'First invocation at ' + new Date();
    callback(null, 'Success');

This is the result from running the above code twice.

First Invocation:

START RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96a Version: $LATEST
2016-12-19T16:08:12.759Z    3c48ced7-9616-11e6-bed8-cb583a02e96a    Global variable not set, setting it now
END RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96a
REPORT RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96a  Duration: 40.26 ms  Billed Duration: 100 ms     Memory Size: 128 MB Max Memory Used: 5 MB

Second Invocation:

START RequestId: 46d7bf5f-9616-11e6-b4a9-bdf95f494d0c Version: $LATEST
2016-12-19T16:08:30.354Z    46d7bf5f-9616-11e6-b4a9-bdf95f494d0c    Global variable: First invocation at Wed Oct 19 2016 16:08:12 GMT+0000 (UTC)
END RequestId: 46d7bf5f-9616-11e6-b4a9-bdf95f494d0c
REPORT RequestId: 46d7bf5f-9616-11e6-b4a9-bdf95f494d0c  Duration: 0.88 ms   Billed Duration: 100 ms     Memory Size: 128 MB Max Memory Used: 5 MB

There are two things to notice with this.

First, be careful using global variables. If you are developing and running the function locally, perhaps using something like lambda-local, node-lambda, or the Serverless Framework, global variables will be uninitialized on each run. But, when running functions deployed on Lambda, global variables can retain their state from one invocation to the next. This can bite you in the aws if you’re not careful.

Suppose you want to time how long your function takes to run. You could write code like this:

var startTime = +new Date();

exports.handler = (event, context, callback) => {
    console.log('The work took %d milliseconds', (+new Date() - startTime));
    callback(null, 'Success');

It will work correctly the first time, but for subsequent times, the start time variable could retain it’s value and give incorrect times in the log. Instead, you should move the variable declaration or initialization inside the handler function.

Second, you can use a global variable for caching. For instance, say we’ve developed a serverless API for a client and one of the functions looks up user permissions in a DynamoDB table. Because we expect the number of users to remain small, instead of looking up the user who invoked the lambda function in the DynamoDB table each time the function is called, we just scan the table (let’s say it only consumes 1 read unit) and cache the result for up to 15 minutes.

This drastically reduces the required provisioned reads needed on the users table, and reduces the Lambda execution time and overall API latency.

Like the sound of that? Check out our implementation on GitHub to learn more.