Reducing AWS Lambda Cold Start Time (Part. 1)
Working almost exclusively with serverless architectures, is always a good idea to keep looking for ways to reduce cost and time. Deploying packages in Node.js and Golang, I’ve noticed that last one has a huge, but a HUGE advantage with cold starts, it executes with minimum duration difference comparing with hot invocations, lets call this duration difference as “Time Delta”. As an experient Node.js developer, I started some experiments to reduce time delta for Node.js packages too, and got a conclusion, there are two main ways to do it:
- Load Less Resources at Memory on Cold Start
- Reduce Package Size
I’m going to divide this subject in two, maybe three posts. First one, this, talking about “Load Less Resources at Memory on Cold Start”, next, a good way to “Reduce Package Size”, and maybe the last, making public and talking about the invocations metrics.
OK, straight to the point.
Load less resources means run less codes which runs immediately at start point, and load them ASAP (as soon as possible) when called and cache them for next calls, something as “lazy load resources”. How?
const lazyAccessor = obj => {
return Object.keys(obj)
.reduce((reduction, key) => {
const callback = obj[key]; if (typeof callback !== 'function') {
throw new Error('callback must be a function.');
}
// for each object property, define passed
// function as an accessor and cache their result
Object.defineProperty(reduction, key, {
get: () => {
// if has a cached version, get it
if (reduction.__cache__[key]) {
return reduction.__cache__[key];
}
// store result at cache and returns
reduction.__cache__[key] = callback(); return reduction.__cache__[key];
}
}); return reduction;
}, {
__cache__: {}
});
};
lazyAccessor(object<string:function>) object<string:accessors>
This function, receives an object where each property is a function which loads the resource, it returns the same object with their accessors (getters) as properties, which when accessed first time, loads the resource into memory. Hard to understand, ok, lets code.
const modules = lazyAccessor({
module1: () => require('module-1'),
module2: () => require('module-2'),
module3: () => require('module-3'),
instanceOfSomething: () => new ClassOfSomething()
});// module 1 was loaded now, while other modules none
modules.module1();
// module 1 was gotten from cache now
modules.module1();
modules.module1();
modules.module1();
modules.module1();// it works with classes too
class ClassOfSomething {
constructor() {
console.log('class constructor was called now!');
} sayHello() {
console.log('Hi There!');
}
}modules.instanceOfSomething.sayHello();
modules.instanceOfSomething.sayHello();
modules.instanceOfSomething.sayHello();// OUTPUT:
// > 'class constructor was called now!'
// > 'Hi There!'
// > 'Hi There!'
// > 'Hi There!'// module 3 was loaded now, while module 2 keeps sleeping
modules.module3();
// module 3 was gotten from cache now
modules.module3();
modules.module3();
modules.module3();
modules.module3();
As a real use case, this technique was used mainly to lazy load database models and schemas, which are heavy to load at memory together, and saying the trhuth, there are no sense on load modules on memory which won’t be acessed during lambda’s life, sometimes consuming CPU resources (as schema validators). And the big advantage of this model is that it is non code intrusive, once you can keep acessing your modules, classes, … whatever via object properties, thanks object accessors.