Deep Dive: Lambda’s Request Payload Size Limit (2/2)

Zac Charles
8 min readAug 24, 2022

--

This is part 2 of a series on Lambda’s payload size limits. Though not required, I suggest starting with the part 1 as I will skip some previously covered details.

Whether you’re having trouble staying within the limit, or are just enthralled by the topic of payload size limits after reading my previous post, you’re in the right place.

In this follow-up, I’ll cover everything you should know to understand Lambda’s request size limit and how it fits with API Gateway.

Let’s go!

Photo by patricia serna on Unsplash

What is the limit?

The Lambda quotas page lists both the request and response payload limits as same 6 MB for synchronous invocations.

In the previous post about response payloads, we learned this actually means 6,291,456 bytes. That’s 6 MiB (mebibytes) plus 100 bytes, or 6 * 1024 * 1024 + 100.

Let’s assume the request limit is the same... What could possibly go wrong?

What is a request payload?

It’s important to really understand how our Lambda function receives its payload.

The Invoke action docs show that Payload is the entire body of the HTTP request and they describe it as “the JSON that you want to provide to your Lambda function as input”.

This description should be interpreted literally:

The JSON that you want to provide…

The HTTP request body must be able to be deserialized as JSON (i.e. using JSON.parse). This means, for example, strings must be wrapped in double-quotes.

…to your Lambda function as input

The parsed payload becomes the event passed to your handler, for example:

  • Python: def handler_name(event, context):
  • JavaScript: exports.handler = async function(event, context) {

Usually event will be an object, but it can actually be any JSON type such as string, number, or an array.

How I’m testing

I’ve created a function named test-function. It stringifies event back to JSON and returns an accurate count of characters and bytes.

An input of "hello" results in a response of “7 bytes in 7 characters”.

I’m using the AWS CLI to invoke the function.

There are many ways to generate the payload, but I’m using Node for consistency with part 1 of this series. I’m calling process.stdout.write instead of console.log to avoid appending an unwanted new-line character.

--payload fileb://input (note: fileb instead of file) tells the CLI to use the raw bytes of the input file as the payload. Reading from a file is needed because of shell argument length limits.

$(tty) tells the CLI to write test-function's output directly to the terminal and >/dev/null discards the Lambda service’s response (except errors).

Proving a point

If you add the --debug argument, the AWS CLI logs details of the HTTP request and response. This provides an quick way to check that it really is sending the payload in the body. The Content-Length header is in there too.

Making request for OperationModel(name=Invoke) with params:
{
"body": b'"AAAAAA"',
"method":"POST",
"url": "https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/test-function/invocations",
...
}

Breaking the limit

When I send 7 million A letters, I get the following error back from Lambda:

An error occurred (RequestEntityTooLargeException) when calling the Invoke operation: Request must be smaller than 6291456 bytes for the InvokeFunction operation

“6,291,456 bytes” is exactly 6 MiB (6 * 1024 * 1024). That’s 100 bytes less than the response limit (6,291,556 bytes).

Lambda’s payload size limit is 100 bytes more for responses than requests, but the docs say they’re the same.

Maybe the response limit is bigger in case you want to return the request alongside something else? For example:

{
"jobCreated": true,
"jobId": 1234,
"request": "AAAAAAAAAAAA...AAAAAAAAA"
}

Making Lambda happy

Based on what we learned in the previous post, we should already know how to make Lambda happy. We send the letter A 6,291,454 times along with two double-quotes to reach the 6,291,456 limit. Right?

Yeah, that’s right. We get the expected response of “6291456 bytes in 6291456 characters” and adding one more A gets us the same error again.

API Gateway: proxy integrations

I’ve modified the example function to return an object with statusCode and body properties like API Gateway expects for proxy integration responses:

Now I can call my test REST API where POST /test is set up as a Lambda proxy integration to test-function.

I’m not even specifying a body/payload and Lambda receives 1,476 bytes.

This is because API Gateway maps requests to a JSON object containing everything it knows about the request and more. This JSON object becomes the payload sent to Lambda.

The object includes all sorts of stuff: request IDs, URL parts, timestamps, configuration, headers, IP addresses, and a lot of null properties for things I’m not even using, like Cognito.

On line 57, you can see body is null. As a JSON string, null uses 4 bytes. If we send in "AA", which is also 4 bytes, we should go from 1476 to 1480 bytes, right?

Wrong! The size has increased 108 bytes.

This is because cURL now sends its default Content-Type: application/x-www-form-urlencoded header which is repeated in both the headers and multiValueHeaders properties. Also, the double-quotes in our "AA" body have been escaped with slashes.

Put another way, we removed null, but added:

  • ,“content-type”:”application/x-www-form-urlencoded”
  • ,”content-type”:[“application/x-www-form-urlencoded”]
  • ”\”AA\””

That’s 4 bytes removed and 112 bytes added; a difference of 108 bytes.

By removing the double quotes from "AA", we also remove the escape slashes. In total, we remove 4 bytes from the prior 1,584.

“The JSON that you want to provide to your Lambda function as input”

Lambda says it wants a JSON payload so we had to add double-quotes when invoking the function directly. But why don’t we need them when invoking via API Gateway?

It’s because API Gateway proxy integrations wrap your body in that JSON object, so Lambda is always happy.

Lost control

The baseline size of the API Gateway proxy integration object is variable.

It will differ drastically based on the headers sent by the client, but also things like Lambda authorizer output, domain name, URL path, query stirng, and even client IP address will have an impact.

This object wrapping/mapping means we don’t have full control over the body that is sent to Lambda and we can’t use the full 6 MiB.

~1,500+ bytes probably isn’t a big deal to most, and the Lambda proxy integration is super convenient (I’d definitely recommend using it where you can), but let’s look at how to take back full control.

API Gateway: AWS Service integration

Before the “Lambda Function” integration (proxy integration) existed, the only way to make API Gateway invoke a Lambda function was using an AWS Service integration.

With no other changes, we’re mostly back to where we were. A request body of "AA" is 4 bytes and omitting the double-quotes results in an error.

API Gateway is just passing our body through to Lambda.

The response includes the object with statusCode & body because API Gateway is no longer doing any proxy integration magic. We have full control over the request and response bodies.

Bonus: emulating the proxy inegration using VTL

We can execise our control using VTL mapping templates. The VTL below instructs API Gateway to send Lambda an object containing body (JSON stringified request body) and headers (containing only the content-type header). Everything else is discarded.

Note: If you’re interested in writing VTL, take a look at Mapping Tool. It’s a professional VTL editor and debugger with intellisense, error checking, etc.

With this request mapping template in place, we’re back down to the 63 bytes of {“body”:”\”AA\””,”headers”:{“content-type”:”application/json”}}.

Importantly, we must now set the Content-Type header to match the template.

Lastly, we can emulate the proxy integration response magic using a response mapping template.

The VTL below overrides the response status code based on the statusCode property and simply outputs whatever is in the body property string.

Skipped: binary payloads

API Gateway also supports an isBase64Encoded property which can also be emulated with VTL, but I’m ignoring that here for simplicity.

Similarly, I deliberately didn’t talk about API Gateway’s handling of binary request payloads. In short, to make them valid JSON, they would be Base64 encoded before being sent to Lambda (proxy integration) or your VTL mapping template (AWS Service integration).

Anyway, all this binary stuff is a topic for another day.

Summary

  • When the Lambda docs say 6 MB, they mean 6,291,456 bytes for requests and 6,291,556 bytes for responses.
  • Lambda functions can return 100 bytes more than they can receive.
  • Lambda request payloads must always be valid JSON.
  • API Gateway proxy integrations map your request body into the body property of a JSON object, so it’s always valid JSON.
  • The size of this object is variable based on headers, API Gateway configuration, and more.
  • You can use the older AWS Service integration to gain full control of request and response payloads.
  • Add the --debug argument to any AWS CLI call to see details of SigV4 signing and the HTTP request/response.

For more like this, please follow me on Medium and Twitter.

--

--