11.2 Synchronous vs Asynchronous Invocation
Right, let’s settle this. The difference between how your Lambda function gets called—synchronously or asynchronously—isn’t just academic. It dictates everything: how you handle errors, how you structure your code, and how much coffee you’ll need when it goes sideways at 2 AM. Get this wrong, and you’re not building on AWS; you’re building a Rube Goldberg machine of failure states.
Think of it like this: when I call you on the phone (synchronous), I wait on the line for you to answer, we talk, and then we hang up. If you don’t answer, I know immediately and can grumble and call someone else. When I send you an email (asynchronous), I fire it off and go about my day. I assume you’ll get to it eventually. If your email inbox is exploding, that’s your problem, not mine.
AWS services invoke your function in one of these two ways, and it’s crucial you know which is which.
The Synchronous Shakedown
This is the “I’m waiting” pattern. The service calling your function (the invoker) sits there, holding a network connection open, waiting for your function to do its thing and return a response. The invoker is literally blocking until you’re done.
Classic examples are API Gateway (for your REST APIs), Amazon Cognito (for user pool triggers), and ELB (Application Load Balancer). You make a request, it triggers the function, and the request to your API doesn’t complete until your function does.
The big deal with synchronous invocation? Errors are the caller’s problem. If your function throws an exception, that exception bubbles all the way back up to the poor user waiting in their browser. The invoker gets the error and has to deal with it.
// A Lambda handler for an API Gateway request
exports.handler = async (event) => {
// Let's say we're validating a payment
const paymentIsValid = validatePayment(event.body);
if (!paymentIsValid) {
// This error message goes RIGHT back to the API caller. Enjoy!
throw new Error("Payment failed. Do you even have money?");
}
return {
statusCode: 200,
body: JSON.stringify({ message: "Success! Congrats on the stuff." })
};
};
The upside is simplicity. The downside is that your function’s execution time is now your user’s waiting time. You also have to handle all your own retries. If the payment service you called in validatePayment is flaky, the user is just sitting there watching a spinner. Not great.
The Asynchronous Advantage (and Annoyance)
This is the “fire and forget” (but not really) pattern. The invoker kicks off your function and immediately gets a 202 Accepted response. It doesn’t wait for you. It doesn’t care about your function’s result. It’s already moved on with its life.
This is how S3 (for new object events), SNS, and CloudWatch Events/Alarms typically work. A file gets dropped in a bucket, and S3 asynchronously invokes your function to process it.
Here’s the critical bit: Errors are your problem. Well, AWS’s problem first, then yours. Since the original caller is long gone, AWS needs a way to handle failures. It does this by using an internal Dead-Letter Queue (DLQ). If your async function fails, AWS will automatically retry it twice (for a total of three attempts). If it still fails, the event payload gets dumped onto a DLQ (either an SQS queue or an SNS topic) that you configured beforehand. It’s your responsibility to have something listening there to handle these failures.
# Your CloudFormation resource defining a Lambda with a DLQ
MyAsyncFunction:
Type: AWS::Lambda::Function
Properties:
...
DeadLetterConfig:
TargetArn: !GetAtt MyDeadLetterQueue.Arn
MyDeadLetterQueue:
Type: AWS::SQS::Queue
The beauty here is decoupling. Your S3 bucket doesn’t get backed up if your image-processing function fails. The system is resilient. The annoyance is that you now have a whole other component to manage (the DLQ) and you’ve introduced eventual consistency. That event will be processed… eventually. Or you’ll get a message about its failure… eventually.
Why You Absolutely Must Care
Mixing these up is a classic way to build a beautifully fragile system.
Pitfall #1: Trying to do a long-running task (like processing a video) in a synchronous context (like API Gateway). API Gateway has a max timeout of 29 seconds. Your user’s connection will likely give up long before that. This is a job for async. Use API Gateway to trigger a Step Function state machine or just send a message to SQS and return a “we’re working on it” message immediately.
Pitfall #2: Assuming idempotency. Especially with async invocations and retries, the same event can be delivered to your function multiple times. Your function logic must be able to handle that without causing duplicate side effects (e.g., charging a customer twice). If your function isn’t idempotent, you’re going to have a bad time.
Pitfall #3: Ignoring the DLQ. Not configuring a Dead-Letter Queue for asynchronous functions is like disconnecting the “check engine” light in your car because the orange glow annoys you. You will lose events. You will have no record of what they were or why they failed. It is not a question of if, but when. Always, always set up a DLQ.