10.4 Handler Functions: Event and Context Objects
Right, let’s talk about the two strange little packages that get delivered to your Lambda function’s door every time it’s invoked: the event and context objects. These are your inputs, your parameters, your window into what’s happening. Understanding them is the difference between a function that works and one that you actually understand why it works.
Think of the event object as the “what.” It’s the payload, the reason your function was called in the first place. Did an image get uploaded to S3? The event will be a JSON object detailing the bucket name, the file key, and a bunch of other metadata. Did an API Gateway request come in? The event will contain the HTTP method, headers, path, and—if you’re lucky—the body of the request. The structure of this object is entirely dependent on what triggered the function. AWS services shove their relevant data into this bag and hand it to you. It’s your job to know how to unpack it.
The context object, on the other hand, is the “who, where, and for how long.” It’s metadata about the function execution itself. This object is provided by the Lambda service, and it gives you crucial information like how much time your function has left to run, its Amazon Resource Name (ARN), the request ID, and more. It’s the function’s internal dashboard.
The Event Object: Your Trigger’s Voice
This is your data. Its shape is a total wild card, so you absolutely must consult the AWS documentation for your specific trigger. The S3 event looks nothing like the SNS event, which looks nothing than a CloudWatch Logs event. Let’s look at a common one: an S3 PUT event.
{
"Records": [
{
"eventVersion": "2.1",
"eventSource": "aws:s3",
"awsRegion": "us-east-1",
"eventTime": "2023-10-27T18:00:00.000Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "EXAMPLE"
},
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"responseElements": {
"x-amz-request-id": "EXAMPLE123456789",
"x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "testConfigRule",
"bucket": {
"name": "example-bucket",
"ownerIdentity": {
"principalId": "EXAMPLE"
},
"arn": "arn:aws:s3:::example-bucket"
},
"object": {
"key": "test/key.txt",
"size": 1024,
"eTag": "0123456789abcdef0123456789abcdef",
"sequencer": "0A1B2C3D4E5F678901"
}
}
}
]
}
Notice it’s wrapped in a Records array. This is because a single Lambda invocation can be triggered by a batch of events (S3 can batch up to… wait for it… one single event. It’s an array of one. Don’t ask me why, it’s just one of those things). Other services, like SQS or Kinesis, will put multiple records in this array for real. Your code should always assume event.Records is an array and iterate through it. The golden nuggets you usually want are record.s3.bucket.name and record.s3.object.key.
The Context Object: The Execution Environment’s Report Card
While the event is about the what, the context is purely about the execution. You don’t control it; the Lambda service does. Its methods and properties are crucial for writing robust functions.
def lambda_handler(event, context):
# Who am I?
function_name = context.function_name
function_version = context.function_version
invoked_function_arn = context.invoked_function_arn
# How much time do I have left? This is VITAL.
remaining_time = context.get_remaining_time_in_millis()
# This is the unique ID for this specific invocation. Gold for debugging.
aws_request_id = context.aws_request_id
print(f"Function {function_name} version {function_version}")
print(f"Request ID: {aws_request_id}")
print(f"Time remaining: {remaining_time}ms")
# ... your business logic here ...
The most important property here, bar none, is get_remaining_time_in_millis(). This is your function’s timer. If you’re doing any kind of long-running process, you must check this value. If you get too close to the timeout, you can gracefully shut down operations, save state, and log a meaningful error instead of just vanishing into the ether with a Task timed out message. It’s the difference between a professional function and an amateur one.
Why Two Objects? Separation of Concerns.
This split is actually brilliant design. The event is your application data. The context is your infrastructure data. This separation keeps things clean. Your business logic should primarily care about the event. The context is for logging, monitoring, and execution control. Muddying the two would be a nightmare.
Common Pitfalls and Best Practices
Assuming the Event Structure: The biggest rookie mistake is hardcoding assumptions about the
eventobject. Always validate its structure. Use something like Pydantic in Python or destructuring with defaults in Node.js to avoidKeyErrororundefinedcrashes. Your function will be invoked by other AWS services you didn’t expect. Trust me.Ignoring the Clock: Not checking
get_remaining_time_in_millis()is like driving a car with a blindfold on. You will eventually crash into the timeout wall. For any function over ~30 seconds, this check is mandatory.Not Using the Request ID: The
context.aws_request_idis the unique identifier for that invocation. Every log message you print should include it. When you’re sifting through CloudWatch Logs trying to find the logs for one specific failure, this ID will be your only lifeline. It’s a free, perfect correlation ID. Use it.The
contextMethods are for Introspection, Not Interaction: You can’t change the timeout with the context object; you can only see how much is left. It’s a read-only interface into the runtime’s state.
So there you have it. The event is the message. The context is the metadata about the messenger and its journey. Respect them both, validate the first, and use the second to write functions that aren’t just successful, but are also well-behaved and debuggable.