29

I've made Lambda functions before but not in Python. I know in Javascript Lambda supports the handler function being asynchronous, but I get an error if I try it in Python.

Here is the code I am trying to test:

async def handler(event, context):
    print(str(event))
    return { 
        'message' : 'OK'
    }

And this is the error I get:

An error occurred during JSON serialization of response: <coroutine object handler at 0x7f63a2d20308> is not JSON serializable
Traceback (most recent call last):
  File "/var/lang/lib/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/var/lang/lib/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/var/lang/lib/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/var/runtime/awslambda/bootstrap.py", line 149, in decimal_serializer
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <coroutine object handler at 0x7f63a2d20308> is not JSON serializable

/var/runtime/awslambda/bootstrap.py:312: RuntimeWarning: coroutine 'handler' was never awaited
  errortype, result, fatal = report_fault(invokeid, e)

EDIT 2021:

Since this question seems to be gaining traction, I assume people are coming here trying to figure out how to get async to work with AWS Lambda as I was. The bad news is that even now more than a year later, there still isn't any support by AWS to have an asynchronous handler in a Python-based Lambda function. (I have no idea why, as NodeJS-based Lambda functions can handle it perfectly fine.)

The good news is that since Python 3.7, there is a simple workaround in the form of asyncio.run:

import asyncio

def lambda_handler(event, context):
    # Use asyncio.run to synchronously "await" an async function
    result = asyncio.run(async_handler(event, context))
    
    return {
        'statusCode': 200,
        'body': result
    }

async def async_handler(event, context):
    # Put your asynchronous code here
    await asyncio.sleep(1)
    
    return 'Success'

Note: The selected answer says that using asyncio.run is not the proper way of starting an asynchronous task in Lambda. In general, they are correct because if some other resource in your Lambda code creates an event loop (a database/HTTP client, etc.), it's wasteful to create another loop and it's better to operate on the existing loop using asyncio.get_event_loop.

However, if an event loop does not yet exist when your code begins running, asyncio.run becomes the only (simple) course of action.

Abion47
  • 22,211
  • 4
  • 65
  • 88
  • 2
    Not that I'm aware of. The programming documentation only indicates the synchronous `def handler(event, context)` option. – jarmod Feb 28 '20 at 18:26
  • `asyncio.run` is not a correct way! You will encounter `Event loop closed` exception at a time of a subsequent invocation. See details in my answer below. – Anton Bryzgalov Sep 12 '21 at 19:04
  • 2
    @AntonBryzgalov I don't know if it's an issue of the Lambda runtime being changed since your answer was written, but when I used `asyncio.get_event_loop()`, it threw an error since there was no existing event loop to get. So you would manually have to create the event loop as well as close it, which was a real pain to manage, which is why I used `asyncio.run` as it handled creating and closing a loop for you. I ran it several times on every supported Python runtime 3.7+ and never got an `Event loop closed` exception. – Abion47 Sep 14 '21 at 15:20
  • @Abion47 Yes, this is possible if you do not attach any resources (like databases connections or HTTP clients) to the loop. Or if you attach all the resources within your Lambda handler function execution (and not in the global Python scope). But defining connections in global scope is recommended cuz it allows you to reuse them through subsequent invocations (and not spend time on re-establishing the connections). – Anton Bryzgalov Sep 20 '21 at 08:36
  • 1
    @AntonBryzgalov Then how do you ensure a loop exists when your lambda doesn't contain any such resources or connections? – Abion47 Sep 20 '21 at 15:13
  • @Abion47 when there are no resources are reused, `asyncio.run` is acceptable. Else you have to use an existing loop (one is always created): `asyncio.get_event_loop()`. – Anton Bryzgalov Sep 26 '21 at 14:59
  • 1
    @AntonBryzgalov One is _**not**_ always created, I just told you that in my own experimentation, `asyncio.get_event_loop` threw an error because there was no event loop to get which is why I needed to use `asyncio.run` in the first place. – Abion47 Sep 26 '21 at 18:45
  • This solution only gives me: Syntax error in module 'app': invalid syntax. – Jeppe Aug 31 '22 at 08:53
  • @Jeppe Invalid syntax where? – Abion47 Aug 31 '22 at 12:44
  • @Abion47 Trying to use asyncio in any way will cause that error, mentioning some line belonging to a file in the asyncio library. – Jeppe Sep 01 '22 at 08:45

2 Answers2

46

Not at all. Async Python handlers are not supported by AWS Lambda.

If you need to use async/await functionality in your AWS Lambda, you have to define an async function in your code (either in Lambda files or a Lambda Layer) and call asyncio.get_event_loop().run_until_complete(your_async_handler()) inside your regular sync Lambda handler:

import asyncio
import aioboto3

# To reduce execution time for subsequent invocations,
#   open a reusable resource in a global scope
dynamodb = aioboto3.Session().resource('dynamodb')

async def async_handler(event, context):
    # Put your asynchronous code here
    table = await dynamodb.Table('test')
    await table.put_item(
        Item={'pk': 'test1', 'col1': 'some_data'},
    )
    return {'statusCode': 200, 'body': '{"ok": true}'}

# Point to this function as a handler in the Lambda configuration
def lambda_handler(event, context):
    loop = asyncio.get_event_loop()
    # DynamoDB resource defined above is attached to this loop:
    #   if you use asyncio.run instead
    #   you will encounter "Event loop closed" exception
    return loop.run_until_complete(async_handler(event, context))

Please note that asyncio.run (introduced in Python 3.7) is not a proper way to call an async handler in AWS Lambda execution environment since Lambda tries to reuse the execution context for subsequent invocations. The problem here is that asyncio.run creates a new EventLoop and closes the previous one. If you have opened any resources or created coroutines attached to the closed EventLoop from previous Lambda invocation you will get «Event loop closed» error. asyncio.get_event_loop().run_until_complete allows you to reuse the same loop. See related StackOverflow question.

AWS Lambda documentation misleads its readers a little by introducing synchronous and asynchronous invocations. Do not mix it up with sync/async Python functions. Synchronous refers to invoking AWS Lambda with further waiting for the result (blocking operation). The function is called immediately and you get the response as soon as possible. Whereas using an asynchronous invocation you ask Lambda to schedule the function execution and do not wait for the response at all. When the time comes, Lambda still will call the handler function synchronously.

Anton Bryzgalov
  • 1,065
  • 12
  • 23
  • 1
    I'm not convinced the `asyncio.run` is not a proper way of doing this. Does it close the previous one? From documentation it seems the current invocation takes care of closing the current one. It does not leak so that the subsequent run has to close the previous one. (Ofc I'm talking about situation where only `asyncio.run` has been used to create any loops.) – Krzysztof Szularz Nov 02 '21 at 09:12
  • 1
    @KrzysztofSzularz closing the current loop (aka the loop from the previous invocation) disallows you to reuse resources attached to the current loop (e.g. `aiohttp` session, database connection). See our discussion in the question comments. – Anton Bryzgalov Nov 04 '21 at 16:34
  • An AWS Lambda function by design processes only a single request at a time. Lambda achieves concurrency by spinning up multiple functions that each processes a single request at a time, so it does not need async handlers. Check out https://ben11kehoe.medium.com/node-is-the-wrong-runtime-for-serverless-jk-c69595f6a8eb – Yusuf Mar 29 '23 at 13:51
  • 2
    @Yusuf async code execution not only allows to serve multiple Lambda requests in parallel, but also to parallelize intra-Lambda IO-bound activities such as calls to AWS services (e.g. writing multiple S3 objects in parallel within a single Lambda call). So, the question is not about serving requests in parallel by a single Lambda function, but about using async capabilities of Python within Lambda execution environment. – Anton Bryzgalov Apr 05 '23 at 10:37
  • This answer: https://stackoverflow.com/a/73367187/1278365 mentions: "Note that run_until_complete, unlike asyncio.run, does not clean up async generators. This is documented in the standard docs." How should we run the async_handler and have the async generators cleaned up? Thanks in advance! – gmagno Jul 24 '23 at 18:09
  • @gmagno you may use `loop.shutdown_asyncgens`: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.shutdown_asyncgens. However, in my answer I assume that you want to reuse all the resources, including, potentially the async generators, in a subsequent invocation. In that context, you may also like `asyncio.TaskGroup` functionality: https://docs.python.org/3/library/asyncio-task.html#task-groups. it waits for underlying tasks to be finished or cancels them explicitly. – Anton Bryzgalov Aug 04 '23 at 10:06
0

Don't use run() method and call run_until_complete()

import json
import asyncio


async def my_async_method():
    await some_async_functionality()


def lambda_handler(event, context):
    loop = asyncio.get_event_loop()    
    result = loop.run_until_complete(my_async_method())
    return {
        'statusCode': 200,
        'body': json.dumps('Hello Lambda')
    }
Aryan Firouzian
  • 1,940
  • 5
  • 27
  • 41
  • This is identical to the chosen answer, and see the comments under the question. The problem with this approach is that it assumes there is an existing event loop to get which is not always the case, hence the need to use `run` to create an event loop. – Abion47 Feb 21 '23 at 14:16