Controlling concurrency of AWS Lambda

Recently I’ve spend significant amount of time on playing with AWS Lambda functions triggered from SQS queue.
You can read the outcome of it in the series of posts:

AWS Lambda events with Severless Framework
Error handling in AWS Lambda triggered by SQS events

One of the benefits of Lambda function is self-scaling.

Given the default settings they can scale up to the limits of user account which is 1000 concurrent invocations per region.

This is the limit for all Lambda functions running in the given account, not a single function, so if you have multiple applications running in the same account they will consume common resources. This is not always desired.

I believe having more control over the resources is important for resilient system.

One of such properties of Lambda function is a timeout which allows you to kill functions which took more time than expected, e.g. hanging in some unrecoverable state.

Another, which allows controlling of concurrent invocations of Lambda on function level, is ReservedConcurrentExecutions property in Cloud Formation or reservedConcurrency in serverless.yaml, e.g.:

handler: handler.hello
name: ${self:provider.stage}-lambdaName
timeout: 10
reservedConcurrency: 5

Such Lambda will be killed if running longer than 10 sec. Maximum of 5 instances running concurrently are allowed.

Let’s do some test to prove it.

First we’ll use an example of Lambda function triggered from SQS queue from my previous post and send some messages.

This should trigger invocation of Lambda.
We will put some sleep time in the function to allow AWS to spin more instances while there are messages to process in the queue.

We should see that without the concurrency limit AWS will continue to start more instances of the function.

Here is an example code:

// receiver.js
exports.handler = (event, context, callback) => {
const interval = setTimeout(() => {
console.log("Wake up!");
callback(null, "");
}, 10000); // delay 10 sec

And no limit is set on lambda concurrency:

handler: receiver.handler
timeout: 30 # timeout in sec, default is 6
- sqs:
- MyQueue
- Arn
- batchSize: 1 # default and max is 10

The whole source code is in Github repository.

We have specified batchSize of 1 to not allow fetching multiple messages at a time by a single Lambda instance. This will give us more concurrent instances with less messages in the queue.

In one terminal we will listen on logs from Cloud Watch:

sls logs -f receiver

In the other we will generate some messages to process.
We can use a simple bash script:

export QUEUE_URL=`aws sqs get-queue-url --queue-name MyQueue --query 'QueueUrl' --output text --profile=sls`
for run in {1..100}
aws sqs send-message --queue-url ${QUEUE_URL} --message-body "test" --profile=sls

This will send total of 100 messages to the SQS queue which will start triggering Lambda functions.
If you like, you can run this script in multiple terminals to generate more messages concurrently.

If we go to receiver lambda function in AWS console and check metrics we would see how the number of concurrent Lambda invocations grows up and then scales down to the initial number.

Lambda running without ReservedConcurrentExecutions

Let’s see what happens if we set the limit.

handler: receiver.handler
timeout: 30 # timeout in sec, default is 6
- sqs:
- MyQueue
- Arn
- batchSize: 1 # default and max is 10
reservedConcurrency: 5

Now the number of concurrently running functions is constant, which can be observed on the chart:

Limited concurrency

- even though Lambda can scale itself up to the account limit of 1000 instances per region, we have possibility to control this with upper limit
- it is good to set timeout on Lambda function to kill tasks running longer than expected
- if Lambda is triggered from SQS we can use `batchSize` to limit number of messages processed in single Lambda invocation