Skip to content

Feature request: Async support for ApiGatewayResolver #3934

Closed
5 of 5 issues completed
Closed

Description

@pkucmus

Use case

I would love to be able to use asynchronous Python with Powertools. I understand there is not as much need for it in a Lambda runtime as a Lambda (process) will handle only one event but I would still be able to use async-only libraries like encode/databases (with the likes of Neon) or simply reduce my execution time by utilizing asyncio.gather when doing concurrent waits for I/O operations.

Solution/User Experience

I imagine being able to use it similarly to what the GraphQL resolver does:

@app.get("/todos")
@tracer.capture_method
async def get_todos():
    todos: httpx.Response = httpx.get("https://jsonplaceholder.typicode.com/todos")
    todos.raise_for_status()
    return {"todos": todos.json()[:10]}


# You can continue to use other utilities just as before
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event: dict, context: LambdaContext) -> dict:
    result = app.resolve(event, context)
    return asyncio.run(result)

Alternative solutions

No response

Acknowledgment

Activity

  1. boring-cyborg commented on Mar 11, 2024

    @boring-cyborg

    Thanks for opening your first issue here! We'll come back to you as soon as we can.
    In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: Invite link

  2. leandrodamascena commented on Mar 12, 2024

    @leandrodamascena
    Contributor

    Hey @pkucmus! Wowww, we're diving into some pretty cool Feature Request here: making our EventHandler and all its resolvers work async and add more flexibility to enhance I/O operations. But it comes with some significant changes ahead.
    One big switch-up is how we handle object serialization. It basically means we'll need to rethink how we call Python functions and make all (or most) of them async. Another challenge is that our resolvers are a kind of Middleware between Lambda execution and the map between specific routes (GET /todos, POST /hello) to execute functions. That adds a bit of extra spice to the mix of how to execute these functions with coroutines.
    And when it comes to our OpenAPI validation, currently, it's all about sync calls. Shifting this to asynchronous operations is quite a significant work.

    To be completely transparent, I'm unable to quantify the extent of work needed to implement this capability right now. Perhaps we should consider drafting an RFC to outline the process in detail. It might even be worth exploring the creation of entirely new resolvers that operate entirely async.

    It's encouraging to see "thumbs up" from many people regarding this matter, and I believe we should seriously consider it. To ensure we stay on track, I'll add some labels such as "revisit in 3 months" and "need help" to keep this issue on our radar and facilitate brainstorming for a solution.

    Thank you and please let us know if you have in mind any kind of implementation or solution for this.

  3. added
    help wantedCould use a second pair of eyes/hands
    revisit-in-3-monthsRequires more customers feedback before making or revisiting a decision
    and removed
    triagePending triage from maintainers
    on Mar 12, 2024
  4. pkucmus commented on Mar 13, 2024

    @pkucmus
    Author

    Thank you @leandrodamascena, what a nice approach for a random idea like that. I would be happy to put in some work into this. Maybe this would not be as replacement for the existing resolvers but something additional? If you would like I could draft something but I would need to understand how it's currently working, i.e. the use of the global state is confusing me a lot - I'm sure there's a reason for it but I don't know why we're storing and accessing the event in a global manner. Is there chance for some guidance or rather I should stay put not to generate more work for you than needed?

    BaseRouter.current_event = self._to_proxy_event(event)
    BaseRouter.lambda_context = context

  5. leandrodamascena commented on Mar 18, 2024

    @leandrodamascena
    Contributor

    Thank you @leandrodamascena, what a nice approach for a random idea like that. I would be happy to put in some work into this. Maybe this would not be as replacement for the existing resolvers but something additional? If you would like I could draft something but I would need to understand how it's currently working

    We're truly appreciate the possibility of you working on a draft for how we can incorporate support for Async in our resolvers and make it work.

    i.e. the use of the global state is confusing me a lot - I'm sure there's a reason for it but I don't know why we're storing and accessing the event in a global manner.

    This code is a little older, probably from when we created the Event Handler utility, but it is still in use. Payloads are different when working with Event Handler, and we store the event and context in the BaseRouter class because we need to access particular fields depending on the type of Resolver, i.e:

    ALB - https://github.com/aws-powertools/powertools-lambda-python/blob/develop/tests/events/albEvent.json
    VPC Lattice - https://github.com/aws-powertools/powertools-lambda-python/blob/develop/tests/events/vpcLatticeEvent.json
    API Gateway REST - https://github.com/aws-powertools/powertools-lambda-python/blob/develop/tests/events/apiGatewayProxyEvent.json

    All of our resolvers inherit from ApiGatewayResolver and ApiGatewayResolver inherits from BaseRouter (the Router class too), so we store this to be easily accessible globally across all Resolvers and dependencies.

    Does this explanation cover what you need, or would you like more information? Feel free to ask if you need further clarification!

    Is there chance for some guidance or rather I should stay put not to generate more work for you than needed?

    Of course, there is! Please feel free to share any questions or blocks you have, and we can collaborate to find the best way to move forward.

    Thank you for taking the time to collaborate on this matter! 🌟

  6. drecdroid commented on Jul 9, 2024

    @drecdroid

    I don't know if this is entirely correct but I'm solving it this way:

    from functools import wraps
    import asyncio
    from aws_lambda_powertools import Tracer
    from aws_lambda_powertools.event_handler import APIGatewayResolver
    
    tracer = Tracer()
    
    app = ApiGatewayResolver()
    
    def wrap_async(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return asyncio.run(func(*args,**kwargs))
        return wrapper
    
    @tracer.capture_method
    @app.get("/hello")
    @wrap_async
    async def hello():
        return { "message": "world" }
    
    @tracer.capture_lambda_handler
    def lambda_handler(event, context):
        return app.resolve(event, context)
  7. sevetseh28 commented on Jul 21, 2024

    @sevetseh28

    Hey @sykire,

    I tried your decorator and it indeed works by leveraging asyncio.run to manage the event loop for each synchronous call to our endpoints. However, this approach seems to counteract the benefits of asynchronous programming due to the significant performance overhead introduced, which I'm yet to find out why (im using aioboto3).

    I also wanted to discuss why incorporating async programming is important, especially in the context of API Gateway. In many projects, I've implemented the API Composition pattern, where the API Gateway is responsible for querying multiple services and aggregating their responses. It's very common API Gateways are responsible for doing this. This model inherently benefits from asynchronous code, as it allows for concurrent calls to multiple services, enhancing efficiency and reducing total response time from the server.

    For instance, if my Lambda function needs to query 5 different AWS services, firing all these requests simultaneously (assuming independence among them) is far more efficient than making sequential calls. Given that Lambda functions already have latency issues by nature, adding synchronous operations only worsens the response time.

    Moreover, I encountered compatibility issues with the X-Ray SDK when using aioboto3 for making concurrent AWS service calls via asyncio.gather. This resulted in an exception: Already ended segment and subsegment cannot be modified. (https://docs.powertools.aws.dev/lambda/python/latest/core/tracer/#concurrent-asynchronous-functions))
    Consequently, I was unable to receive a successful response or track the trace for this case.

    By not using asyncio.gather and just making a single asynchronous AWS call didn’t improve the situation either. Surprisingly, endpoint response times increased significantly, from approximately 800 ms to 1700 ms, when querying DynamoDB asynchronously. This suggests that the use of asyncio.run could be introducing an overhead, but I can't confirm by how much. I'll do more tests. A difference of 800ms is too much. I'll update as soon as I have more results.

  8. 8 remaining items

  9. leandrodamascena commented on Apr 6, 2026

    @leandrodamascena
    Contributor

    Hey everyone! I did a deep investigation into what it takes to make this work properly. I want to be very transparent about the complexity and share the plan I have on top of my mind.

    Current situation

    The entire resolve() chain today is synchronous - from route matching through the middleware stack to response serialization. A simple asyncio.run() wrapper around the handler (like the workarounds shared by @drecdroid and @sevetseh28) works for basic cases, but it bypasses our middleware chain, doesn't work with OpenAPI validation, and has the X-Ray issues you've already experienced.

    To do this right, we need to build a parallel async middleware chain alongside the existing sync one.

    I don't think everything needs to be async because some operations like JSON parsing, Pydantic validation, response serialization, and compression are all CPU-bound and not I/O bound, so, there is nothing to do here with async. Async only helps with I/O (DynamoDB, S3, HTTP calls - the stuff you actually want to parallelize). So the existing sync operations stay sync. We only need async for the call structure so we can await the user's handler at the right point.

    Here's how the async chain looks internally:

    resolve_async(event, context)
      |-- _to_proxy_event()                    sync (CPU)
      |-- _resolve_async()                     async (propagates await)
      |     '-- _call_route_async()            async
      |           '-- async middleware chain
      |                 |-- RequestValidation     sync (json.loads + pydantic = CPU)
      |                 |-- user middlewares       sync or async (auto-detect)
      |                 |-- ResponseValidation    sync (pydantic = CPU)
      |                 '-- api_adapter_async
      |                       |-- result = handler(**args)
      |                       |-- if coroutine: await result   <-- the async point
      |                       '-- _to_response(result)         sync (CPU)
      '-- ResponseBuilder.build()              sync (json.dumps + zlib = CPU)
    

    The new implementation

    I'm not promising an exactly date here, but I want to make a dedicated release in Q3 2026 with two phases.

    Phase 1 - Core async support:

    • New resolve_async() method - existing resolve() stays untouched
    • AsyncMiddlewareFrame that auto-detects sync vs async middlewares
    • Async-aware _registered_api_adapter that awaits coroutine results
    • Sync and async handlers work in the same app, same middleware chain
    • Full OpenAPI validation support with async handlers

    Phase 2 - Tracer/observability:

    • X-Ray SDK + asyncio.gather compatibility (the Already ended segment issue @sevetseh28 hit)
    • If X-Ray SDK can't support it, guidance on OpenTelemetry as alternative
    • @tracer.capture_method for async functions already exists

    An example

    app = APIGatewayHttpResolver()
    
    # Sync — works exactly as before
    @app.get("/health")
    def health():
        return {"status": "ok"}
    
    # Async — native support
    @app.get("/dashboard/<user_id>")
    async def get_dashboard(user_id: str):
        orders, profile = await asyncio.gather(
            get_orders(user_id),
            get_profile(user_id),
        )
        return {"orders": orders, "profile": profile}
    
    def lambda_handler(event, context):
        return asyncio.run(app.resolve_async(event, context))

    Existing sync middlewares will continue working without any changes. Async middlewares will also be supported for those who need them.

    I appreciate everyone's patience and the workarounds you've shared - they helped me understand the real-world usage patterns. As always, contributions and feedback are welcome.

  10. removed
    on-holdThis item is on-hold and will be revisited in the future
    on Apr 27, 2026
  11. added
    pending-releaseFix or implementation already in dev waiting to be released
    and removed
    help wantedCould use a second pair of eyes/hands
    on Apr 27, 2026
  12. github-actions commented on May 4, 2026

    @github-actions
    Contributor

    This is now released under 3.29.0 version!

  13. removed
    pending-releaseFix or implementation already in dev waiting to be released
    on May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions