View a markdown version of this page

Performance best practices for the Amazon SDK for .NET - Amazon SDK for .NET (V4)
Services or capabilities described in Amazon Web Services documentation might vary by Region. To see the differences applicable to the China Regions, see Getting Started with Amazon Web Services in China (PDF).

Version 4 (V4) of the Amazon SDK for .NET has been released!

For information about breaking changes and migrating your applications, see the migration topic.

Orange button with text "Click here for details".

Performance best practices for the Amazon SDK for .NET

How you create clients, handle responses, and configure your application has a large effect on throughput, latency, and memory use. This topic describes usage and configuration patterns that help your applications run efficiently and reliably. These patterns matter most under high load or in resource-constrained environments such as containers and serverless functions. Following these practices can improve performance and prevent common problems such as slow responses, hangs, and high memory use.

The most impactful practices are the following:

Before you begin, be sure you have set up your environment and configured your project.

Reuse a single, long-lived service client

Service clients such as AmazonS3Client and AmazonDynamoDBClient are thread-safe, relatively expensive to construct, and long-lived. Constructing a client resolves Region and endpoint information and establishes the underlying HTTP infrastructure. Create one client per service and reuse it for the lifetime of your application. If you call more than one Region, create a separate client for each Region.

Warning

Do not create a new service client for each request or inside a loop. Creating clients repeatedly churns the underlying HTTP infrastructure, which adds measurable latency. It can also exhaust sockets or handles and cause requests to fail or hang under load.

In applications that use dependency injection, register the client as a singleton. The AddAWSService extension method from the AWSSDK.Extensions.NETCore.Setup NuGet package registers the client with a default lifetime of ServiceLifetime.Singleton. The client is created the first time it's requested, and the same instance is reused for the lifetime of the process. For more information about registering Amazon services with dependency injection and reading options from configuration, see AWSSDK.Extensions.NETCore.Setup and IConfiguration.

You can also register a client as a singleton manually.

builder.Services.AddSingleton<IAmazonS3>(_ => new AmazonS3Client());
Note

Because AddAWSService registers the client as a singleton by default, do not dispose the client that it provides. If you need a non-default lifetime, pass a different ServiceLifetime value to the optional lifetime parameter of AddAWSService.

Reusing the client does not mean you should stop disposing per-operation responses. Reuse the client for the life of the application, but continue to dispose the responses and streams that individual operations return, as described in Dispose responses and streams.

Dispose responses and streams to release connections

Some SDK response objects carry a live network stream. The most common example is GetObjectResponse, which implements IDisposable and exposes the object's content through its ResponseStream property. The response holds an open HTTP connection until the stream is fully read or the response is disposed. If you leak these responses (for example, by calling GetObject in a loop without disposing each result), open connections accumulate until the connection pool is exhausted, and the next call blocks. This is the cause behind reports of downloads that "hang randomly" or stall on the Nth object.

Always wrap a streaming response in a using statement and read or copy its stream promptly.

using Amazon.S3; using Amazon.S3.Model; // s3Client is a reused, long-lived client. var request = new GetObjectRequest { BucketName = bucketName, Key = key }; using var response = await s3Client.GetObjectAsync(request); await response.WriteResponseStreamToFileAsync(filePath, append: false, CancellationToken.None);

Disposing the response and disposing its ResponseStream are equivalent; either one closes the underlying network stream and returns the connection to the pool.

Tip

If you only need object metadata, such as size, last-modified time, content type, or ETag, call GetObjectMetadata or GetObjectMetadataAsync instead of GetObject. The metadata operation issues an HTTP HEAD request and transfers no object body, so there's no content stream to manage.

var metadata = await s3Client.GetObjectMetadataAsync(bucketName, key); Console.WriteLine($"Size: {metadata.ContentLength} bytes");
Warning

Response and stream types such as GetObjectResponse don't implement a finalizer, so you can't rely on garbage collection to release their connections for you. You must dispose responses and streams deterministically with using or an explicit Dispose call.

Use async/await correctly

On modern .NET, the service operations in the Amazon SDK for .NET are asynchronous and return a Task. On .NET Framework, synchronous methods also exist, but asynchronous calls scale better and are recommended. Consume operations with await, and propagate async through every layer of your code up to the entry point. For more information about asynchronous programming with the SDK, see Asynchronous programming.

Warning

Do not block on an asynchronous SDK call with .Result, .Wait(), or .GetAwaiter().GetResult(). This sync-over-async pattern is a common cause of applications that hang:

  • Under load, blocking calls consume threads faster than the thread pool can grow, so continuations can't run. This thread-pool starvation appears as an indefinite hang and is the dominant failure mode on modern .NET (where ASP.NET Core has no default SynchronizationContext).

  • Some contexts capture a SynchronizationContext, such as classic ASP.NET, Windows Forms, WPF, Blazor WebAssembly, and .NET Framework applications. In these contexts, blocking the calling thread while a continuation needs that same thread produces a deadlock. Operations in the SDK use ConfigureAwait(false) internally, so they don't post their continuations back to the captured context. The deadlock arises from async code elsewhere in your call chain that does capture it. Consuming SDK calls with await throughout avoids the problem entirely.

The following method blocks on the asynchronous call and can deadlock or starve the thread pool.

// Anti-pattern: do not do this. public GetObjectResponse Get(GetObjectRequest request) { return s3Client.GetObjectAsync(request).Result; }

Instead, make the method async and await the call.

public async Task<GetObjectResponse> GetAsync(GetObjectRequest request) { return await s3Client.GetObjectAsync(request); }

Additional recommendations for asynchronous code:

  • Pass a CancellationToken to every operation so a slow or stalled call can be canceled instead of hanging. For more information, see Using the CancellationToken parameter for timeouts.

  • Don't swallow exceptions in an empty catch block. Doing so hides the real failure and makes a hang indistinguishable from an error. Catch specific exceptions and log them.

  • When rethrowing a caught exception, use throw; rather than throw ex; so that the original stack trace is preserved.

  • If you must call from a synchronous boundary, treat it as a last resort and isolate the work from the captured context rather than making blocking calls the default pattern.

Configure .NET garbage collection for Amazon Lambda and Amazon ECS

When a container appears to have a "memory leak," the .NET garbage collector (GC) might be retaining reclaimed memory for reuse. As a result, process memory can appear high and stable even when the managed heap is not growing. In addition, the GC doesn't automatically detect a container's memory limit (its cgroup limit). In a constrained environment, the heap might then grow toward the host's memory rather than the container's limit. This can lead to OutOfMemoryException or the container being terminated.

To help the GC work well in constrained environments:

  • Set an explicit memory limit on the container so the GC honors the cgroup limit, and/or set the DOTNET_GCHeapHardLimit (an absolute byte value, in hexadecimal) or DOTNET_GCHeapHardLimitPercent environment variable to cap the managed heap. In several reported Amazon ECS out-of-memory cases, setting a hard memory limit resolved the crashes.

  • On small Amazon Lambda and Amazon ECS hosts, consider disabling concurrent (background) garbage collection so the collector doesn't reserve additional memory; for example, set the DOTNET_gcConcurrent environment variable to 0, or set <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection> in the project file.

  • Bound your concurrency. Launching many operations at once, such as calling Task.WhenAll over a large collection, inflates process memory and can starve the connection pool. Cap the degree of parallelism instead. For example, avoid this unbounded pattern:

    // Anti-pattern: starts one task per item with no limit. await Task.WhenAll(keys.Select(key => s3Client.GetObjectMetadataAsync(bucket, key)));

    Instead, cap concurrency with Parallel.ForEachAsync:

    var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; await Parallel.ForEachAsync(keys, options, async (key, token) => { await s3Client.GetObjectMetadataAsync(bucket, key, token); });

For more information about these settings, see Runtime configuration options for garbage collection on learn.microsoft.com. For guidance specific to Amazon compute, see the Amazon Developer Tools Blog post Configuring .NET Garbage Collection for Amazon ECS and Amazon Lambda.

Manage HTTP connections and connection limits

Under high throughput, two connection-related problems are common. The first is opening too many short-lived connections. This exhausts ephemeral ports, leaves sockets in TIME_WAIT, and adds TCP and TLS handshake latency. The second is having too few connections available, which bottlenecks parallelism. Reusing a single, long-lived client (see Reuse service clients) is the foundation for healthy connection pooling, because pooling depends on the client being reused.

To tune the number of concurrent connections per endpoint, set the MaxConnectionsPerServer property on the client configuration. When this property is null (the default), the underlying HttpClientHandler default applies, which is effectively unlimited on modern .NET. Raise it only when many concurrent requests to the same endpoint are bottlenecked on connections. A good starting point is the peak number of concurrent requests you expect per endpoint. Setting it far higher than your workload needs wastes sockets without improving throughput.

using Amazon.S3; var config = new AmazonS3Config { MaxConnectionsPerServer = 50 }; var s3Client = new AmazonS3Client(config);

If you already use dependency injection, configure your clients through AWSSDK.Extensions.NETCore.Setup. This is the recommended approach when you use DI or register several service clients. It centralizes configuration and makes clients easy to inject and test. You can set configuration values from your application's configuration instead of in code. For more information, see AWSSDK.Extensions.NETCore.Setup and IConfiguration.

Finally, bound your own parallelism so that you don't start more concurrent operations than your connection limit allows. For example, gate calls with a SemaphoreSlim sized to your connection limit:

var throttle = new SemaphoreSlim(50); // match MaxConnectionsPerServer await throttle.WaitAsync(token); try { await s3Client.GetObjectAsync(request, token); } finally { throttle.Release(); }

Configure timeouts and retries

Timeouts and retries have a direct effect on perceived performance. A Timeout value that is too high lets a stalled request block for a long time. When a service is already returning throttling errors, an aggressive retry policy adds more requests and can make the throttling worse. Choose a retry policy that fits your application's tolerance for latency versus failure, and let genuine exceptions propagate rather than retrying in a way that masks a hang.

Note

The Timeout property does not affect asynchronous calls. If you are using asynchronous calls, see Using the CancellationToken parameter for timeouts instead.

For more information about the retry modes, MaxErrorRetry, and the Timeout property (ReadWriteTimeout applies only to .NET Framework), along with examples of how to set them, see Retries and timeouts.

Optimize streaming and large-object transfers (Amazon S3)

To upload and download large objects, or many objects, use the TransferUtility class in the Amazon.S3.Transfer namespace. It uploads and downloads in parallel using multipart transfers, and manages streams, parts, and connections for you. This is faster than a single-stream transfer and is the recommended way to move large objects.

  • Parallel multipart download. Earlier versions of the SDK downloaded an object as a single stream, rather than downloading parts in parallel. Starting with AWSSDK.S3 version 4.0.17, TransferUtility provides multipart (parallel) download through the DownloadWithResponseAsync, OpenStreamWithResponseAsync, and DownloadDirectoryWithResponseAsync methods. When you use OpenStreamWithResponseAsync, the object's parts are buffered in memory while you consume the returned stream. Control how many parts are buffered with the MaxInMemoryParts property of TransferUtilityOpenStreamRequest. For most transfers, prefer TransferUtility. Download byte ranges yourself with the ByteRange property of GetObjectRequest only when you need a specific range or a custom parallelism scheme. For more information, see Introducing multipart download support for Amazon SDK for .NET Transfer Manager on the Amazon Developer Tools Blog.

  • Content length for uploads. An Amazon S3 PUT requires a known content length, and by default the SDK computes a checksum over the request body. When the length is known and the stream is seekable, the SDK can do this without buffering the whole object in memory. The TransferUtility handles non-seekable streams for you by buffering as needed. If you instead call PutObjectAsync directly with a non-seekable stream (such as a raw request body in ASP.NET Core), the request can fail. Provide a seekable stream, set the content length explicitly, or configure how the SDK calculates checksums. For more information, see Data integrity protections in the Amazon SDKs and Tools Reference Guide.

  • Part sizing. Amazon S3 allows a maximum of 10,000 parts per multipart upload. When the total length is known, the SDK computes a part size that stays within this limit automatically. You mainly need to set PartSize yourself for streams whose length isn't known in advance. Otherwise, small default parts can exceed the limit on a very large upload. A larger part size also reduces per-part overhead, at the cost of more memory per part.

Diagnose performance problems

When you investigate a slowdown, hang, or apparent leak, the following signals help you find the cause quickly:

  • A growing number of sockets in the CLOSE_WAIT or TIME_WAIT state (visible with netstat) is the fingerprint of undisposed responses or of clients being created and discarded per request. See Dispose responses and streams and Reuse service clients.

  • Enable request metrics and response logging to confirm that requests are actually being dispatched and to measure latency. Set the properties of the LoggingConfig object on AWSConfigs before you create your service clients. A client captures settings such as LogMetrics when it is constructed.

    using Amazon; AWSConfigs.LoggingConfig.LogMetrics = true; AWSConfigs.LoggingConfig.LogResponses = ResponseLoggingOption.OnError;
  • When investigating memory, distinguish whole-process memory from the managed heap. High but stable process memory is often the GC holding reclaimed memory rather than a leak. See Configure garbage collection.