Java Serverless on AWS Lambda · Frameworks, Native Images and SDK Clients

AWS SDK for Java v2: Clients, HTTP and Retries

Why creating SDK clients per request makes a Java function slow and leaky, and how to configure clients for Lambda.

Scenario

Example app: a Java function reads from DynamoDB and publishes to SQS. Each invocation builds new clients with DynamoDbClient.create() and SqsClient.create() and never closes them.

What you see in production

Every request is 150 to 300 ms slower than necessary; after many invocations in a long-lived environment, memory use creeps up and occasional Unable to execute HTTP request: Timeout waiting for connection from pool errors appear.

The naive code

public String handleRequest(Map<String, String> input, Context ctx) {
    DynamoDbClient dynamo = DynamoDbClient.create();     // credential chain, region lookup, new HTTP pool
    SqsClient sqs = SqsClient.create();
    var item = dynamo.getItem(r -> r.tableName("orders").key(key(input)));
    sqs.sendMessage(r -> r.queueUrl(QUEUE_URL).messageBody(toJson(item)));
    return "ok";                                         // clients never closed
}

Reproduce it

Per-request clients versus shared clients
public class Main {
    static int clientsCreated = 0, openPools = 0;

    static final class Client {
        Client() { clientsCreated++; openPools++; }
    }

    public static void main(String[] args) {
        int invocations = 1000;
        int setupMsPerClient = 120;                                   // illustrative

        for (int i = 0; i < invocations; i++) { new Client(); new Client(); }
        System.out.printf("per request: %d clients, %d open pools, %d ms spent on setup%n",
            clientsCreated, openPools, clientsCreated * setupMsPerClient);

        clientsCreated = 0; openPools = 0;
        Client dynamo = new Client(), sqs = new Client();             // static fields: created at init
        System.out.printf("shared:      %d clients, %d open pools, %d ms spent on setup%n",
            clientsCreated, openPools, clientsCreated * setupMsPerClient);
    }
}

Output

per request: 2000 clients, 2000 open pools, 240000 ms spent on setup
shared:      2 clients, 2 open pools, 240 ms spent on setup

Run this on your computer — see “Get Started” for setup.

Root cause

SDK clients are heavyweight: building one resolves credentials and region through provider chains and creates an HTTP client with a connection pool. Clients are thread-safe and designed to be long-lived and shared. Creating them per request repeats the setup and, without close(), leaks connection pools and threads.

The fix

Create clients once (static fields or the handler's constructor), configured explicitly for Lambda:

public class OrderHandler implements RequestHandler<Map<String, String>, String> {
    private static final SdkHttpClient HTTP = AwsCrtHttpClient.builder().build();
    private static final DynamoDbClient DYNAMO = DynamoDbClient.builder()
        .region(Region.of(System.getenv("AWS_REGION")))
        .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
        .httpClient(HTTP)
        .overrideConfiguration(c -> c.retryStrategy(AwsRetryStrategy.standardRetryStrategy())
                                     .apiCallTimeout(Duration.ofSeconds(3)))
        .build();

    @Override
    public String handleRequest(Map<String, String> input, Context ctx) {
        var item = DYNAMO.getItem(r -> r.tableName("orders").key(key(input)));
        // ...
        return "ok";
    }
}

Set timeouts (apiCallTimeout, apiCallAttemptTimeout) shorter than the function timeout, and keep SDK retries modest so that, combined with Lambda's own retries for asynchronous sources, they do not multiply load during an outage.

Trade-offs

  • The AWS CRT HTTP client starts fast and performs well but is a native library (check architecture support); UrlConnectionHttpClient is lightest but has fewer features; the Apache client has the most options.
  • Async clients (DynamoDbAsyncClient) enable concurrent calls but add complexity; virtual threads with sync clients are an alternative on Java 21.
  • Explicit credentials providers are faster but must match how the function runs (Lambda provides credentials through environment variables).

Detect it in production

  • Per-request latency with an SDK setup component in traces; connection-pool timeout errors.
  • Thread and memory growth over an environment's life.
  • Retry counts in SDK metrics; total attempts during downstream incidents.

Key points

  • SDK clients are heavyweight and thread-safe: create once, share, reuse.
  • Configure region, credentials provider, HTTP client and timeouts explicitly.
  • Keep SDK retries modest to avoid retry multiplication.

Further reading: AWS SDK for Java v2: reduce SDK startup time for Lambda · AWS SDK for Java v2: HTTP clients

Practice questions

Why should AWS SDK for Java v2 clients be created once per execution environment?

Answer

Clients are expensive to build (credential and region resolution, HTTP client and connection pool) and are thread-safe for reuse; creating them per request adds latency and, if not closed, leaks resources.

Which HTTP client options exist for the SDK on Lambda, and how do they differ?

Answer

The AWS CRT-based client (fast start-up and high performance, native library), UrlConnectionHttpClient (lightweight, minimal features) and the Apache client (most configurable, heavier); Netty is used for async clients.

How can SDK retries and Lambda retries multiply load during an outage?

Answer

For asynchronous or event-source invocations, Lambda retries the whole invocation, and inside each attempt the SDK retries every call. Several layers of retries multiply requests to the struggling dependency; keep SDK retries modest and set timeouts.

Open in the app to mark lessons complete, take notes and track your progress.