github aws-powertools/powertools-lambda-python v3.31.0

4 hours ago

Summary

This release adds a new Circuit Breaker utility (in alpha) that stops your Lambda from sending requests to an unhealthy downstream and gives it time to recover. We also made parameter validation in the Event Handler more flexible, so any Pydantic Field annotation now works with any parameter type.

A huge thanks to everyone who helped shape the Circuit Breaker RFC and reviewed this release!

Circuit Breaker (alpha)

Docs

When a downstream service is failing, retries and Lambda's scaling only make it worse: more clients sending requests to something that is already down. The Circuit Breaker stops sending traffic to an unhealthy dependency, then probes it to see when it is safe to resume.

It ships as circuit_breaker_alpha on purpose. We want about a month of real-world feedback before we lock the public API and promote it to GA.

The smallest setup is a persistence store and a name. You wrap the function that makes the downstream call:

from aws_lambda_powertools.utilities.circuit_breaker_alpha import circuit_breaker
from aws_lambda_powertools.utilities.circuit_breaker_alpha.persistence import (
    CircuitBreakerDynamoDBPersistence,
)
from aws_lambda_powertools.utilities.typing import LambdaContext

persistence = CircuitBreakerDynamoDBPersistence(table_name="CircuitBreakerState")


@circuit_breaker(name="payment-backend", persistence_store=persistence)
def charge(order: dict) -> dict:
    return payment_api.charge(order)


def lambda_handler(event: dict, context: LambdaContext) -> dict:
    return charge(event)

With no config, sensible defaults apply: open after 5 failures in a row, probe after 30s, close after 3 successes, and treat any exception as a failure. A few things make it a good fit for Lambda:

  • It's free when healthy. The failure counter lives in memory, so a healthy circuit writes nothing. We only save state when it changes, so you pay during an incident, which is when you want to.
  • State is shared across environments. Circuit state lives in DynamoDB, so the first environment that opens the circuit protects all the others.
  • It fails open. If the state store can't be reached, the request goes through. A circuit breaker should never become the outage it's meant to prevent.
  • One probe on recovery. When the timer is up, only one environment is selected to test the backend, instead of all of them sending requests at the same time.

When the circuit is open, you decide what happens to the rejected request with an on_circuit_open callback (buffer it, drop it, return a cached value), or let it raise CircuitBreakerOpenError. You can also watch state changes with an on_transition hook to emit your own metrics.

import json
from uuid import uuid4

from aws_lambda_powertools.utilities.circuit_breaker_alpha import circuit_breaker
from aws_lambda_powertools.utilities.circuit_breaker_alpha.persistence import (
    CircuitBreakerDynamoDBPersistence,
)
from aws_lambda_powertools.utilities.typing import LambdaContext

persistence = CircuitBreakerDynamoDBPersistence(table_name="CircuitBreakerState")


def buffer_payload(payload: dict, circuit) -> None:
    # Circuit is OPEN. The call never ran, so the payload is yours to handle.
    s3.put_object(Bucket="payment-overflow", Key=f"{circuit.name}/{uuid4()}", Body=json.dumps(payload))


@circuit_breaker(
    name="payment-backend",
    persistence_store=persistence,
    on_circuit_open=buffer_payload,
)
def charge(order: dict) -> dict:
    return payment_api.charge(order)


def lambda_handler(event: dict, context: LambdaContext) -> dict:
    return charge(event)

Field annotations in parameter validation

Docs

You can now use any Pydantic Field annotation with any parameter location (Path, Query, Header, Body), the same way it works inside a model. Before, only Field(discriminator=...) with Body() was supported.

from typing import Annotated

from pydantic import Field

from aws_lambda_powertools.event_handler import APIGatewayHttpResolver
from aws_lambda_powertools.event_handler.openapi.params import Query
from aws_lambda_powertools.utilities.typing import LambdaContext

app = APIGatewayHttpResolver(enable_validation=True)


@app.get("/count")
def get_count(n: Annotated[int, Field(gt=0), Query]):  # gt=0 enforced, 422 on n <= 0
    return {"count": n}


def lambda_handler(event: dict, context: LambdaContext) -> dict:
    return app.resolve(event, context)

Last but not least, thanks to everyone who reported issues and helped us improve this release.

Changes

📜 Documentation updates

🔧 Maintenance

This release was made possible by the following contributors:

@dependabot[bot], @github-actions[bot], @leandrodamascena, dependabot[bot] and github-actions[bot]

Don't miss a new powertools-lambda-python release

NewReleases is sending notifications on new releases.