|
| 1 | +--- |
| 2 | +title: Lambda Metadata |
| 3 | +description: Utility |
| 4 | +--- |
| 5 | + |
| 6 | +Lambda Metadata provides idiomatic access to the Lambda Metadata Endpoint (LMDS), eliminating boilerplate code for retrieving execution environment metadata like Availability Zone ID. |
| 7 | + |
| 8 | +## Key features |
| 9 | + |
| 10 | +* Retrieve Lambda execution environment metadata with a single method call |
| 11 | +* Automatic caching for the sandbox lifetime, avoiding repeated HTTP calls |
| 12 | +* Thread-safe access for concurrent executions (compatible with [Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank"}) |
| 13 | +* Automatic [SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html){target="_blank"} cache invalidation via [CRaC](https://openjdk.org/projects/crac/){target="_blank"} integration |
| 14 | +* Lightweight with minimal external dependencies, using built-in `HttpURLConnection` |
| 15 | +* GraalVM support |
| 16 | + |
| 17 | +## Getting started |
| 18 | + |
| 19 | +### Installation |
| 20 | + |
| 21 | +=== "Maven" |
| 22 | + |
| 23 | + ```xml hl_lines="3-7" |
| 24 | + <dependencies> |
| 25 | + ... |
| 26 | + <dependency> |
| 27 | + <groupId>software.amazon.lambda</groupId> |
| 28 | + <artifactId>powertools-lambda-metadata</artifactId> |
| 29 | + <version>{{ powertools.version }}</version> |
| 30 | + </dependency> |
| 31 | + ... |
| 32 | + </dependencies> |
| 33 | + ``` |
| 34 | + |
| 35 | +=== "Gradle" |
| 36 | + |
| 37 | + ```groovy hl_lines="6" |
| 38 | + repositories { |
| 39 | + mavenCentral() |
| 40 | + } |
| 41 | + |
| 42 | + dependencies { |
| 43 | + implementation 'software.amazon.lambda:powertools-lambda-metadata:{{ powertools.version }}' |
| 44 | + } |
| 45 | + |
| 46 | + sourceCompatibility = 11 |
| 47 | + targetCompatibility = 11 |
| 48 | + ``` |
| 49 | + |
| 50 | +### IAM Permissions |
| 51 | + |
| 52 | +No additional IAM permissions are required. The Lambda Metadata Endpoint is available within the Lambda execution environment and uses a Bearer token provided automatically via environment variables. |
| 53 | + |
| 54 | +### Basic usage |
| 55 | + |
| 56 | +Retrieve metadata using `LambdaMetadataClient.get()`: |
| 57 | + |
| 58 | +=== "App.java" |
| 59 | + |
| 60 | + ```java hl_lines="1 2 9 10" |
| 61 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 62 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 63 | + |
| 64 | + public class App implements RequestHandler<Object, String> { |
| 65 | + |
| 66 | + @Override |
| 67 | + public String handleRequest(Object input, Context context) { |
| 68 | + // Fetch metadata (automatically cached after first call) |
| 69 | + LambdaMetadata metadata = LambdaMetadataClient.get(); |
| 70 | + String azId = metadata.getAvailabilityZoneId(); // e.g., "use1-az1" |
| 71 | + |
| 72 | + return "{\"az\": \"" + azId + "\"}"; |
| 73 | + } |
| 74 | + } |
| 75 | + ``` |
| 76 | + |
| 77 | +!!! info "At launch, only `availabilityZoneId` is available. The API is designed to support additional metadata fields as LMDS evolves." |
| 78 | + |
| 79 | +### Caching behavior |
| 80 | + |
| 81 | +Metadata is **cached automatically** after the first call. Subsequent calls return the cached value without making HTTP requests. |
| 82 | + |
| 83 | +=== "CachingExample.java" |
| 84 | + |
| 85 | + ```java hl_lines="9 12" |
| 86 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 87 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 88 | + |
| 89 | + public class CachingExample implements RequestHandler<Object, String> { |
| 90 | + |
| 91 | + @Override |
| 92 | + public String handleRequest(Object input, Context context) { |
| 93 | + // First call: fetches from endpoint and caches |
| 94 | + LambdaMetadata metadata = LambdaMetadataClient.get(); |
| 95 | + |
| 96 | + // Subsequent calls: returns cached value (no HTTP call) |
| 97 | + LambdaMetadata metadataAgain = LambdaMetadataClient.get(); |
| 98 | + |
| 99 | + // Both return the same cached instance |
| 100 | + assert metadata == metadataAgain; |
| 101 | + |
| 102 | + return "{\"az\": \"" + metadata.getAvailabilityZoneId() + "\"}"; |
| 103 | + } |
| 104 | + } |
| 105 | + ``` |
| 106 | + |
| 107 | +This is safe because metadata (like Availability Zone) never changes during a sandbox's lifetime. |
| 108 | + |
| 109 | +## Advanced |
| 110 | + |
| 111 | +### Eager loading at module level |
| 112 | + |
| 113 | +For predictable latency, fetch metadata at class initialization: |
| 114 | + |
| 115 | +=== "EagerLoadingExample.java" |
| 116 | + |
| 117 | + ```java hl_lines="7" |
| 118 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 119 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 120 | + |
| 121 | + public class EagerLoadingExample implements RequestHandler<Object, String> { |
| 122 | + |
| 123 | + // Fetch during cold start (class loading) |
| 124 | + private static final LambdaMetadata METADATA = LambdaMetadataClient.get(); |
| 125 | + |
| 126 | + @Override |
| 127 | + public String handleRequest(Object input, Context context) { |
| 128 | + // No latency hit here - already cached |
| 129 | + return "{\"az\": \"" + METADATA.getAvailabilityZoneId() + "\"}"; |
| 130 | + } |
| 131 | + } |
| 132 | + ``` |
| 133 | + |
| 134 | +#### SnapStart considerations |
| 135 | + |
| 136 | +When using [SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html){target="_blank"}, the function may restore in a different Availability Zone. The utility automatically handles this by registering with CRaC to invalidate the cache after restore. |
| 137 | + |
| 138 | +Using the same eager loading pattern above, the cache is automatically invalidated on SnapStart restore, ensuring subsequent calls to `LambdaMetadataClient.get()` return refreshed metadata. |
| 139 | + |
| 140 | +!!! note "For module-level usage with SnapStart, ensure `LambdaMetadataClient` is referenced during initialization so the CRaC hook registers before the snapshot is taken." |
| 141 | + |
| 142 | +### Lambda Managed Instances |
| 143 | + |
| 144 | +For [Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html){target="_blank"} (multi-threaded concurrency), no changes are needed. The utility uses thread-safe caching with `AtomicReference` to ensure correct behavior across concurrent executions on the same instance. |
| 145 | + |
| 146 | +=== "ManagedInstanceHandler.java" |
| 147 | + |
| 148 | + ```java hl_lines="9" |
| 149 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 150 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 151 | + |
| 152 | + public class ManagedInstanceHandler implements RequestHandler<Object, String> { |
| 153 | + |
| 154 | + @Override |
| 155 | + public String handleRequest(Object input, Context context) { |
| 156 | + // Thread-safe: multiple concurrent invocations safely share cached metadata |
| 157 | + LambdaMetadata metadata = LambdaMetadataClient.get(); |
| 158 | + return "{\"az\": \"" + metadata.getAvailabilityZoneId() + "\"}"; |
| 159 | + } |
| 160 | + } |
| 161 | + ``` |
| 162 | + |
| 163 | +### Error handling |
| 164 | + |
| 165 | +The utility throws `LambdaMetadataException` when the metadata endpoint is unavailable or returns an error: |
| 166 | + |
| 167 | +=== "ErrorHandlingExample.java" |
| 168 | + |
| 169 | + ```java hl_lines="2 7 14 18 21" |
| 170 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 171 | + import software.amazon.lambda.powertools.metadata.exception.LambdaMetadataException; |
| 172 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 173 | + import software.amazon.lambda.powertools.logging.Logging; |
| 174 | + import org.slf4j.Logger; |
| 175 | + import org.slf4j.LoggerFactory; |
| 176 | + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; |
| 177 | + |
| 178 | + public class ErrorHandlingExample implements RequestHandler<Object, String> { |
| 179 | + |
| 180 | + private static final Logger LOG = LoggerFactory.getLogger(ErrorHandlingExample.class); |
| 181 | + |
| 182 | + @Override |
| 183 | + @Logging |
| 184 | + public String handleRequest(Object input, Context context) { |
| 185 | + String az; |
| 186 | + try { |
| 187 | + LambdaMetadata metadata = LambdaMetadataClient.get(); |
| 188 | + az = metadata.getAvailabilityZoneId(); |
| 189 | + } catch (LambdaMetadataException e) { |
| 190 | + LOG.warn("Could not fetch metadata", entry("statusCode", e.getStatusCode()), entry("error", e.getMessage())); |
| 191 | + az = "unknown"; |
| 192 | + } |
| 193 | + |
| 194 | + return "{\"az\": \"" + az + "\"}"; |
| 195 | + } |
| 196 | + } |
| 197 | + ``` |
| 198 | + |
| 199 | +## Testing your code |
| 200 | + |
| 201 | +When running outside a Lambda execution environment (e.g., in unit tests), the `AWS_LAMBDA_METADATA_API` and `AWS_LAMBDA_METADATA_TOKEN` environment variables are not available. Calling `LambdaMetadataClient.get()` in this context throws a `LambdaMetadataException`. |
| 202 | + |
| 203 | +### Mocking LambdaMetadataClient |
| 204 | + |
| 205 | +For tests where you need to control the metadata values, use Mockito's `mockStatic` to mock `LambdaMetadataClient.get()`: |
| 206 | + |
| 207 | +=== "MockedMetadataTest.java" |
| 208 | + |
| 209 | + ```java hl_lines="15-17" |
| 210 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 211 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 212 | + import org.mockito.MockedStatic; |
| 213 | + import org.junit.jupiter.api.Test; |
| 214 | + import static org.assertj.core.api.Assertions.assertThat; |
| 215 | + import static org.mockito.Mockito.*; |
| 216 | + |
| 217 | + class MockedMetadataTest { |
| 218 | + |
| 219 | + @Test |
| 220 | + void shouldUseMetadataInHandler() { |
| 221 | + LambdaMetadata mockMetadata = mock(LambdaMetadata.class); |
| 222 | + when(mockMetadata.getAvailabilityZoneId()).thenReturn("use1-az1"); |
| 223 | + |
| 224 | + try (MockedStatic<LambdaMetadataClient> mockedClient = |
| 225 | + mockStatic(LambdaMetadataClient.class)) { |
| 226 | + mockedClient.when(LambdaMetadataClient::get).thenReturn(mockMetadata); |
| 227 | + |
| 228 | + App handler = new App(); |
| 229 | + String result = handler.handleRequest(null, null); |
| 230 | + |
| 231 | + assertThat(result).contains("use1-az1"); |
| 232 | + } |
| 233 | + } |
| 234 | + } |
| 235 | + ``` |
| 236 | + |
| 237 | +### Using WireMock |
| 238 | + |
| 239 | +For integration tests, you can use [WireMock](https://wiremock.org/){target="_blank"} to mock the metadata HTTP endpoint. Set `AWS_LAMBDA_METADATA_API` and `AWS_LAMBDA_METADATA_TOKEN` environment variables using [junit-pioneer](https://junit-pioneer.org/docs/environment-variables/){target="_blank"}, and stub the endpoint response: |
| 240 | + |
| 241 | +=== "WireMockMetadataTest.java" |
| 242 | + |
| 243 | + ```java hl_lines="10-12" |
| 244 | + import static com.github.tomakehurst.wiremock.client.WireMock.*; |
| 245 | + import static org.assertj.core.api.Assertions.assertThat; |
| 246 | + |
| 247 | + import com.github.tomakehurst.wiremock.junit5.WireMockTest; |
| 248 | + import org.junitpioneer.jupiter.SetEnvironmentVariable; |
| 249 | + import org.junit.jupiter.api.Test; |
| 250 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 251 | + import software.amazon.lambda.powertools.metadata.internal.LambdaMetadataHttpClient; |
| 252 | + |
| 253 | + @WireMockTest(httpPort = 8089) |
| 254 | + @SetEnvironmentVariable(key = "AWS_LAMBDA_METADATA_API", value = "localhost:8089") |
| 255 | + @SetEnvironmentVariable(key = "AWS_LAMBDA_METADATA_TOKEN", value = "test-token") |
| 256 | + class WireMockMetadataTest { |
| 257 | + |
| 258 | + @Test |
| 259 | + void shouldFetchMetadataFromEndpoint() { |
| 260 | + stubFor(get(urlEqualTo("/2026-01-15/metadata/execution-environment")) |
| 261 | + .withHeader("Authorization", equalTo("Bearer test-token")) |
| 262 | + .willReturn(aResponse() |
| 263 | + .withStatus(200) |
| 264 | + .withHeader("Content-Type", "application/json") |
| 265 | + .withBody("{\"AvailabilityZoneID\": \"use1-az1\"}"))); |
| 266 | + |
| 267 | + LambdaMetadataHttpClient client = new LambdaMetadataHttpClient(); |
| 268 | + LambdaMetadata metadata = client.fetchMetadata(); |
| 269 | + |
| 270 | + assertThat(metadata.getAvailabilityZoneId()).isEqualTo("use1-az1"); |
| 271 | + } |
| 272 | + } |
| 273 | + ``` |
| 274 | + |
| 275 | +## Using with other Powertools utilities |
| 276 | + |
| 277 | +Lambda Metadata integrates seamlessly with other Powertools utilities to enrich your observability data with Availability Zone information. |
| 278 | + |
| 279 | +=== "IntegratedExample.java" |
| 280 | + |
| 281 | + ```java |
| 282 | + import software.amazon.lambda.powertools.logging.Logging; |
| 283 | + import software.amazon.lambda.powertools.tracing.Tracing; |
| 284 | + import software.amazon.lambda.powertools.tracing.TracingUtils; |
| 285 | + import software.amazon.lambda.powertools.metrics.FlushMetrics; |
| 286 | + import software.amazon.lambda.powertools.metrics.Metrics; |
| 287 | + import software.amazon.lambda.powertools.metrics.MetricsFactory; |
| 288 | + import software.amazon.lambda.powertools.metrics.model.MetricUnit; |
| 289 | + import software.amazon.lambda.powertools.metadata.LambdaMetadata; |
| 290 | + import software.amazon.lambda.powertools.metadata.LambdaMetadataClient; |
| 291 | + import org.slf4j.Logger; |
| 292 | + import org.slf4j.LoggerFactory; |
| 293 | + import org.slf4j.MDC; |
| 294 | + |
| 295 | + public class IntegratedExample implements RequestHandler<Object, String> { |
| 296 | + |
| 297 | + private static final Logger LOG = LoggerFactory.getLogger(IntegratedExample.class); |
| 298 | + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); |
| 299 | + |
| 300 | + @Logging |
| 301 | + @Tracing |
| 302 | + @FlushMetrics(captureColdStart = true) |
| 303 | + @Override |
| 304 | + public String handleRequest(Object input, Context context) { |
| 305 | + LambdaMetadata metadata = LambdaMetadataClient.get(); |
| 306 | + String azId = metadata.getAvailabilityZoneId(); |
| 307 | + |
| 308 | + // Add AZ as dimension for all metrics |
| 309 | + metrics.addDimension("availability_zone_id", azId); |
| 310 | + |
| 311 | + // Add AZ to structured logs |
| 312 | + MDC.put("availability_zone_id", azId); |
| 313 | + LOG.info("Processing request"); |
| 314 | + |
| 315 | + // Add AZ to traces |
| 316 | + TracingUtils.putAnnotation("availability_zone_id", azId); |
| 317 | + |
| 318 | + // Add metrics |
| 319 | + metrics.addMetric("RequestProcessed", 1, MetricUnit.COUNT); |
| 320 | + |
| 321 | + return "{\"status\": \"ok\"}"; |
| 322 | + } |
| 323 | + } |
| 324 | + ``` |
0 commit comments