Stateless Doesn't Mean No State!

Joel Haubold Trek10
Joel Haubold | Jan 02 2017

Mon, 02 Jan 2017

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:

<pre><code>START RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96a Version: $LATEST2016-12-19T16:08:12.759Z 3c48ced7-9616-11e6-bed8-cb583a02e96a Global variable not set, setting it nowEND RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96aREPORT RequestId: 3c48ced7-9616-11e6-bed8-cb583a02e96a Duration: 40.26 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 5 MB</code></pre>

*Second Invocation:*

<pre><code>START RequestId: 46d7bf5f-9616-11e6-b4a9-bdf95f494d0c Version: $LATEST2016-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-bdf95f494d0cREPORT RequestId: 46d7bf5f-9616-11e6-b4a9-bdf95f494d0c Duration: 0.88 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 5 MB</code></pre>

**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) => { doTheWork(); 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.