AWS Serverless Common Mistakes - Coding (1/7)

Mistakes you do while coding serverless solutions. This is part of a multipart blog post about serverless common mistakes. Friday, February 15, 2019

When switching technology, there is always the danger that your ship will hit the rocks. It is significantly less dangerous when switching to serverless, but there are dangers as with any technology.

This is a list of mistakes that others did when building serverless solutions. I also added some additional mistakes I managed to make. So, do not repeat them. I split the list into several posts. The list might be long, but do not be scared. Just have the list available when you are architecting your solution and you will be fine. If you made a list of possible mistakes you can do in other technologies, it would be much longer. Why? Because in serverless, you manage less, so there are less possible mistakes.

Content:

They are not ordered by importance.

Choosing Language

C# and Java are mastodon in lean and stripped-down environments such as AWS Lambda. They have a longer cold start and, on average, require higher memory functions. Try to use Node.js, Python, or Go. If you do not care about cold starts because your functions run in backend in an asynchronous environment and do not face the user then C# and Java are also a good choice. Especially if you are using special libraries or your team is versed in those environments.

C# and Java are mastodon in lean and stripped-down environments such as AWS Lambda

Separate Core Logic from Lambda Handlers

Lambda handlers are entry points to function calls. Keep them small and separate core logic to other modules. It is quite possible that you will have to reuse logic in different Lambdas. This would also ease migrating your solution to any other platform if you need to.

Separate core logic from Lambda handlers

Small Package Size, as Few Dependencies as Possible, and Bundling

A large package size causes a longer cold start. The difference is not significant, but there is a significant difference in deployment time. Strive to have as small a package size as possible. That consequently means as few libraries as possible. This is especially important for Java, where developers are used to including a lot of large libraries.

When deploying in Node.js, think about using a bundler serverless-plugin-optimize or serverless-webpack. This reduces the number of unused dependencies and makes your code smaller. If you do not do that, you will copy every folder in node_modules for each library.

When deploying, exclude AWS SDK, because it is already there. If you use a serverless-webpack, add this to webpack.config.js:

module.exports = {
  ...
  externals: [
    /aws-sdk/ // Available on AWS Lambda
  ],
};
In Lambda use as few dependencies as possible

In Node.js, Use Promise Version of Calls and Async/Await Pattern

In Node.js, use promise versions of API instead of callbacks, which works perfectly with async/await pattern. Before Node.js 8, bluebird provided the utility to convert callback-based functions to promise-based. But in Node.js >= 8, you have the "util" module that does that:

const fs = require('fs')
const { promisify } = require('util')
const readFile = promisify(fs.readFile)

AWS SDK also support promises:

const AWS = require('aws-sdk')
const Lambda = new AWS.Lambda()
async function invokeLambda(functionName) {
  const req = {
    FunctionName: functionName,
    Payload: JSON.stringify({ message: 'hello world' })
  }
  await Lambda.invoke(req).promise()
}

When you are using Node.js, use the async/await pattern as the code is much cleaner. Async/await is supported in Node >= 8, so you can use it in AWS Lambda. Try to use TypeScript, which makes larger programs much easier to write and maintain. When using async/await, you must be aware not to make some common mistakes:

  • Don't forget await when calling async function. That is a bug that is not easy to spot. You get promise instead of result and you do not know why. For example, at the end of function you save results, but if you do not wait for saving to be finished, they may never be saved.
  • Do not write sequential code.
    const result1 = await work1();
    const result2 = await work2();
    
    You are unnecessary waiting for work1 to finish before starting work2. This is better:
    const result1Promise = work1();
    const result2Promise = work2();
    const result1 = await result1Promise;
    const result2 = await result2Promise;
    
  • Do not use async/await inside a forEach. It does not wait for work to be finished.
    [1, 2, 3].forEach(async (x) => {
    await sleep(x)
    })
    
  • For more information, see Common Node8 mistakes in Lambda
Do not do async/await mistakes in Lambda using #Nodejs

Function Granulation

How granular your function should be is a long discussion. The general rule is that function should do one thing only. If you have smaller functions, they run faster, you have a shorter cold start, and you can prioritize functions on critical paths so you can give them more memory, which also means a faster processor and network. Smaller functions are easier to manage, but do not go too far.

Function should do one thing only is general best practice

More functions mean more functions to manage and slower deployment. Cold starts for each function could add up in a low traffic environment when each call is waiting for the previous to finish. Another consideration is deploying a lot of functions. If you are using AWS CloudFormation for deploying functions (Serverless Framework is also using it), you could hit a hard limit of 200 resources. Because each function could mean more than one resource, you may hit the limit at 30–35 functions or less.

Organize Resources

Building serverless means creating a lot of functions and other resources such as DynamoDB tables, SNS topics, SQS queues, and so on. That could mean a lot of mess if you don't know when each thing belongs to which part of the system. Proper naming and documenting are crucial to prevent ending up with a lot of resources you are afraid to remove because you do not know if they are in use. And if you leave them, they could present a security risk. If you are already in this situation, you can write a program that discovers unused AWS Lambda functions.

Functions Always Timeout

If you're implementing your Lambdas using Node.js, there is the possibility that your functions may timeout, even after seeming to have completed their processing. The reason for this is the JavaScript event loop may not have cleared. A common example is when you want to reuse a database connection. When this happens, you will end up paying for the full period of time until the function times out (the default timeout is 3 seconds) rather than just the time doing the actual processing.

To fix this, set the following as the first line of code in your handler function:

context.callbackWaitsForEmptyEventLoop = false;

If set to false, AWS Lambda will freeze the process. Outstanding events will continue to run during the next invocation if AWS Lambda chooses to use the frozen process.

Linux Environment

Does your code depend on an operating system environment? For example, libraries that transform images, or a computer vision and machine learning software library like OpenCV. Lambda is executed in Linux and if you develop in any other system then you could have a problem. In that case, make sure you upload an OS-specific module or develop in a Linux environment similar to the one Lambda runs in. For distributing external libraries, you could use Lambda Layers.