<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged lambda</title>
<link>https://coderlegion.com/tag/lambda</link>
<description>Powered by Question2Answer</description>
<item>
<title>AWS Step Functions: Creating a ‘Busy Waiting’ flow to wait for successful lambda executions.</title>
<link>https://coderlegion.com/31106/aws-step-functions-creating-a-busy-waiting-flow-to-wait-for-successful-lambda-executions</link>
<description>&lt;p&gt;Hi everyone, welcome to this article. In the past few days, I had to create a logic in AWS Step functions which involved waiting for a lambda that queries athena before going forward in the flow. &lt;/p&gt;
&lt;p&gt;When I was discussing the logic, it felt like an easy thing to accomplish in minutes but I was surprised upon knowing that Step Functions does not have its native technique/solve to wait for a state to succeed before starting another. This made me search for hours before coming up with a solution.&lt;/p&gt;
&lt;p&gt;Here, I will share with you a technique which will help you in the future.&lt;/p&gt;
&lt;p&gt;First of all, I will create a simple lambda function. All we need to know from the lambda is whether it did its job. So, this simple function will make sure to return its status.&lt;/p&gt;
&lt;p&gt;Lambda function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

def lambda_handler(event, context):
    response = {
        'status': 'success',
        'message': 'Hello, World!'
    }
    
    return response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can copy both the function and the Step function definition which I will share below to replicate all of this in your own environment. &lt;/p&gt;
&lt;p&gt;Now, lets go ahead and create a state machine in AWS Step Functions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vqducgkmq06ude209gj.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on ‘Create state machine’. &lt;/p&gt;
&lt;p&gt;Choose the option to write your own definition and choose ‘standard’.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3mcoo8lc3omc8jbhnh7h.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, in the definition section, copy and paste the following definition.&lt;/p&gt;
&lt;p&gt;Step Functions definition:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Comment&quot;: &quot;A Hello World example of the Amazon States Language using Pass states&quot;,
  &quot;StartAt&quot;: &quot;Main&quot;,
  &quot;States&quot;: {
    &quot;Main&quot;: {
      &quot;Type&quot;: &quot;Task&quot;,
      &quot;Resource&quot;: &quot;arn:aws:states:::lambda:invoke&quot;,
      &quot;OutputPath&quot;: &quot;$.Payload&quot;,
      &quot;Parameters&quot;: {
        &quot;Payload.$&quot;: &quot;$&quot;,
        &quot;FunctionName&quot;: &quot;arn:aws:lambda:us-east-1:031342435657:function:stepfunctionlambda_test:$LATEST&quot;
      },
      &quot;Retry&quot;: [
        {
          &quot;ErrorEquals&quot;: [
            &quot;Lambda.ServiceException&quot;,
            &quot;Lambda.AWSLambdaException&quot;,
            &quot;Lambda.SdkClientException&quot;,
            &quot;Lambda.TooManyRequestsException&quot;
          ],
          &quot;IntervalSeconds&quot;: 2,
          &quot;MaxAttempts&quot;: 6,
          &quot;BackoffRate&quot;: 2
        }
      ],
      &quot;Next&quot;: &quot;WaitforInvocation&quot;
    },
    &quot;WaitforInvocation&quot;: {
      &quot;Type&quot;: &quot;Wait&quot;,
      &quot;Seconds&quot;: 2,
      &quot;Next&quot;: &quot;CheckInvocationsStatus&quot;
    },
    &quot;CheckInvocationsStatus&quot;: {
      &quot;Type&quot;: &quot;Choice&quot;,
      &quot;Choices&quot;: [
        {
          &quot;Variable&quot;: &quot;$.status&quot;,
          &quot;StringEquals&quot;: &quot;success&quot;,
          &quot;Next&quot;: &quot;Final&quot;
        }
      ],
      &quot;Default&quot;: &quot;WaitforInvocation&quot;
    },
    &quot;Final&quot;: {
      &quot;Type&quot;: &quot;Succeed&quot;
    }
  }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the definition and click next.&lt;/p&gt;
&lt;p&gt;Specify the name of your state machine and then leave everything else as default. A new execution role will be created if you don’t specify any.&lt;/p&gt;
&lt;p&gt;Save it and then you will have created a state machine which looks like this:&lt;/p&gt;
&lt;p&gt;Visual flow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h567w7r69dda3w38t36g.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;To explain the definition above, I have pointed out how this state machine works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The state machine starts at the &quot;Main&quot; state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &quot;Main&quot; state is a Task state that invokes a Lambda function specified by the &quot;FunctionName&quot; parameter. The input to the Lambda function is the entire input passed to the state machine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &quot;OutputPath&quot; parameter is  set to &quot;$.Payload&quot; to capture the output of the Lambda function.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the Lambda function invocation fails with certain exceptions specified in the &quot;Retry&quot; section (Lambda.ServiceException, Lambda.AWSLambdaException, Lambda.SdkClientException,  Lambda.TooManyRequestsException), the state machine will retry the invocation up to 6 times with a backoff interval of 2 seconds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After invoking the Lambda function, the state machine proceeds to the &quot;WaitforInvocation&quot; state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &quot;WaitforInvocation&quot; state is a Wait state that pauses the execution for 2 seconds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After the wait period,  the state machine goes  to the &quot;CheckInvocationsStatus&quot; state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &quot;CheckInvocationsStatus&quot; state is a Choice state that checks  the value of the &quot;$.status&quot; field in the output of the Lambda function. If the status is &quot;success&quot;, the state machine proceeds to  the &quot;Final&quot; state. Otherwise, it goes back to the &quot;WaitforInvocation&quot; state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &quot;Final&quot; state is a Succeed state that indicates the successful completion of the state machine.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Now, lets go ahead and execute it!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/amyu3l4skfdr3os7owe5.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on ‘Start execution’ and  leave the input field empty.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7hib8yqc4i674d0132r8.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Incase of a lambda that requires input, you can insert your input here as a Json&lt;/p&gt;
&lt;p&gt;After you start your execution, each state will turn into different colours which indicates the following.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Green: Indicates successful completion of a state or step.&lt;/li&gt;
&lt;li&gt;Red: Indicates a failure or error occurred during the execution of a state or step.&lt;/li&gt;
&lt;li&gt;Yellow: Indicates a state is in a waiting or paused state.&lt;/li&gt;
&lt;li&gt;Blue: Represents a decision or choice point where the state machine evaluates conditions to determine the next state.&lt;/li&gt;
&lt;li&gt;Gray: Represents a passive state that simply passes input to output without performing significant work.&lt;/li&gt;
&lt;li&gt;White: Represents transitions or connections between states, indicating the flow of execution from one state to another.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally this is how the execution will look like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fp8g4hthee521cc883w4.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Thank you. Hope you can now go ahead and apply the  “Busy Waiting” logic in your step functions.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31106/aws-step-functions-creating-a-busy-waiting-flow-to-wait-for-successful-lambda-executions</guid>
<pubDate>Wed, 14 Oct 2026 18:01:55 +0000</pubDate>
</item>
<item>
<title>Building a worker-evaluator retry loop in LangGraph (and where it bites you on Lambda)</title>
<link>https://coderlegion.com/22871/building-a-worker-evaluator-retry-loop-in-langgraph-and-where-it-bites-you-on-lambda</link>
<description>&lt;p&gt;Most agent demos stop at &quot;the model called a tool and gave an answer.&quot; That answer is often wrong, and nothing in the loop notices. I wanted an agent that checks its own work against a success criteria before returning, and retries when it falls short. That is the whole idea behind Wingman: a worker that does the task, and a separate evaluator that decides whether the task is actually done.&lt;/p&gt;
&lt;p&gt;Here is how the loop is built, the one decision that shaped everything, and the part that broke once it ran on Lambda behind API Gateway.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;A single-pass agent has no idea when it failed. It calls a tool, produces text, and returns. If the output misses the point, the user finds out, not the system.&lt;/p&gt;
&lt;p&gt;I wanted a gate. Something that reads the assistant's last response, compares it against a stated success criteria, and either accepts it or sends the worker back to try again with feedback. Two roles, not one. The worker produces. The evaluator judges.&lt;/p&gt;
&lt;p&gt;The catch is that a retry loop with no ceiling is a bill waiting to happen. So the loop needs a hard stop, and it needs to know the difference between &quot;the assistant needs to try again&quot; and &quot;the assistant is stuck and should ask the user.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-approach-and-the-key-decision&quot;&gt;The approach and the key decision&lt;/h3&gt;
&lt;p&gt;The graph has three nodes: &lt;code&gt;worker&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, and &lt;code&gt;evaluator&lt;/code&gt;. The state carries the conversation, the success criteria, the latest feedback, two boolean flags, and a turn counter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class State(TypedDict):
    messages: Annotated[List[Any], add_messages]
    success_criteria: str
    feedback_on_work: Optional[str]
    success_criteria_met: bool
    user_input_needed: bool
    turn_count: int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker runs first. If its last message contains tool calls, the router sends it to the tool node and back. If not, it goes to the evaluator.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def worker_router(self, state: State) -&amp;gt; str:
    last = state[&quot;messages&quot;][-1]
    return &quot;tools&quot; if (hasattr(last, &quot;tool_calls&quot;) and last.tool_calls) else &quot;evaluator&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key decision is making the evaluator a structured-output call, not a free-text one. It returns a typed object, so the routing logic reads clean booleans instead of parsing prose.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class EvaluatorOutput(BaseModel):
    feedback: str = Field(description=&quot;Feedback on the assistant's response&quot;)
    success_criteria_met: bool = Field(description=&quot;Whether the success criteria have been met&quot;)
    user_input_needed: bool = Field(
        description=&quot;True if more input is needed from the user, or the assistant is stuck&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both the worker and the evaluator run on &lt;code&gt;gpt-4o-mini&lt;/code&gt;. The worker is bound to tools. The evaluator is bound to the schema above with &lt;code&gt;.with_structured_output&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The routing after evaluation is where the loop's whole personality lives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def route_based_on_evaluation(self, state: State) -&amp;gt; str:
    if state[&quot;success_criteria_met&quot;] or state[&quot;user_input_needed&quot;]:
        return &quot;END&quot;
    if state.get(&quot;turn_count&quot;, 0) &amp;gt;= self.MAX_TURNS:
        return &quot;END&quot;
    return &quot;worker&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three ways out. The criteria is met. The assistant is stuck and needs the user. Or the loop has spent its five turns. Otherwise it loops back to the worker, and this time the worker gets the feedback baked into its system prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if state.get(&quot;feedback_on_work&quot;):
    system_message += f&quot;&quot;&quot;
Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.
Feedback:
{state[&quot;feedback_on_work&quot;]}
Please continue the assignment...&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That feedback injection is what makes the retry worth anything. The worker does not just try again blind. It tries again knowing why the last attempt was rejected.&lt;/p&gt;
&lt;p&gt;One design detail I care about: the evaluator is told not to confuse &quot;not done yet&quot; with &quot;stuck.&quot; A failed criteria means retry, not stop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;For user_input_needed: set True ONLY if the assistant explicitly asked the user a question or stated it cannot proceed.
Do NOT set user_input_needed=True just because the criteria was not met — the assistant should simply retry.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without that line, the evaluator bails to the user the first time the worker misses, and the retry loop never actually retries.&lt;/p&gt;
&lt;h3 id=&quot;what-broke-and-what-i-changed&quot;&gt;What broke, and what I changed&lt;/h3&gt;
&lt;p&gt;The loop is stateless by design. Wingman reconstructs the full conversation from DynamoDB on every request and runs a fresh graph invocation with a throwaway thread ID, so there is no in-memory state to lose between Lambda cold starts. That part held up.&lt;/p&gt;
&lt;p&gt;The timeout did not.&lt;/p&gt;
&lt;p&gt;A worker-evaluator loop is slow. Five turns, each turn a worker call plus tool calls plus an evaluator call, adds up to real wall-clock time. I set the Lambda timeout to 300 seconds to give the loop room:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# LangGraph agent loops can take a while.
timeout     = 300  # 5 minutes (Lambda max is 15 min)
memory_size = 1024
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That number is a lie if the request comes through API Gateway. The HTTP API has a hard 29-second integration cap that Lambda's 300 seconds cannot override. So a long agent run would keep working inside Lambda while the gateway had already given up on the caller. The function billed for the full run. The user got nothing.&lt;/p&gt;
&lt;p&gt;The fix was to stop routing long tasks through the gateway at all. I added a Lambda Function URL, which is a direct HTTPS endpoint with no 29-second ceiling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# Lambda function URL — direct HTTPS invoke, bypasses API Gateway 29s timeout.
resource &quot;aws_lambda_function_url&quot; &quot;backend&quot; {
  function_name      = aws_lambda_function.backend.function_name
  authorization_type = &quot;NONE&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the gateway stays for the fast endpoints, and the agent loop can run against the Function URL without a proxy in the middle deciding it took too long.&lt;/p&gt;
&lt;p&gt;There is a second sharp edge I have not fully closed. &lt;code&gt;turn_count&lt;/code&gt; only increments inside the evaluator node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;turn_count&quot;: state.get(&quot;turn_count&quot;, 0) + 1,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the worker can loop &lt;code&gt;worker -&amp;gt; tools -&amp;gt; worker -&amp;gt; tools&lt;/code&gt; several times before it ever hands off to the evaluator, and none of those hops count against &lt;code&gt;MAX_TURNS&lt;/code&gt;. So the five-turn cap is really &quot;five evaluator turns,&quot; not &quot;five model calls.&quot; A task that keeps calling tools without producing a final answer can burn far more time than the cap implies. The honest fix is to count worker iterations too, or add a wall-clock budget inside the run, rather than trusting the evaluator counter to bound cost. That one is still on my list.&lt;/p&gt;
&lt;h3 id=&quot;takeaway&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;An evaluator that gates the worker's output is the cheap half of a self-correcting agent. The expensive half is making the retry ceiling account for every model call, not just the ones you happened to count.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22871/building-a-worker-evaluator-retry-loop-in-langgraph-and-where-it-bites-you-on-lambda</guid>
<pubDate>Fri, 17 Jul 2026 05:44:06 +0000</pubDate>
</item>
<item>
<title>The Truth About Serverless APIs Nobody Mentions</title>
<link>https://coderlegion.com/19091/the-truth-about-serverless-apis-nobody-mentions</link>
<description>&lt;p&gt;I've noticed serverless conversations tend to split into two camps:&lt;/p&gt;
&lt;p&gt;People who think it solves everything, and people who think it's a mistake.&lt;/p&gt;
&lt;p&gt;The reality is much more useful than either extreme.&lt;/p&gt;
&lt;p&gt;Everyone who sells you on serverless leads with the same three points: no servers to manage, automatic scaling, and you only pay for what you use.&lt;/p&gt;
&lt;p&gt;All of that is technically true.&lt;/p&gt;
&lt;p&gt;None of it is the whole story.&lt;/p&gt;
&lt;p&gt;I've shipped serverless APIs in production — Go Lambdas behind API Gateway, the kind that power real user-facing products. I'm not here to talk you out of it. For the right workload, it's genuinely excellent.&lt;/p&gt;
&lt;p&gt;But the gaps between the pitch and the reality are specific, and knowing them in advance changes the decisions you make early, when they're still cheap to change.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;cold-starts-are-real-just-not-how-you-think&quot;&gt;Cold starts are real — just not how you think&lt;/h3&gt;
&lt;p&gt;Lambda cold starts happen when AWS needs to provision a new execution environment for your function. The runtime boots, your init code runs, and then the handler executes.&lt;/p&gt;
&lt;p&gt;For a Go function on arm64, that's typically 50–150ms total. For Node.js or Python with heavy dependencies, it can be 500ms–2 seconds.&lt;/p&gt;
&lt;p&gt;The part nobody leads with: cold starts are probabilistic, not guaranteed.&lt;/p&gt;
&lt;p&gt;Under steady traffic, most invocations hit warm instances. You'll rarely see them in your P50 or even P95. Where they show up is P99 and beyond — the tail latency your best users experience right after a traffic spike, or the first request after a quiet weekend.&lt;/p&gt;
&lt;p&gt;The practical advice: profile in production with real traffic before you add Provisioned Concurrency. It costs money and adds complexity.&lt;/p&gt;
&lt;p&gt;For most APIs, the smarter move is optimizing your init code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lazy-load dependencies&lt;/li&gt;
&lt;li&gt;keep your binary small&lt;/li&gt;
&lt;li&gt;initialize only what the first request actually needs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fix the actual problem before reaching for the expensive solution.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-billing-math-will-surprise-you&quot;&gt;The billing math will surprise you&lt;/h3&gt;
&lt;p&gt;Lambda pricing is simple on paper: you pay per request and per GB-second of execution time.&lt;/p&gt;
&lt;p&gt;At low traffic, it's nearly free.&lt;/p&gt;
&lt;p&gt;The surprise comes at scale — or in specific usage patterns.&lt;/p&gt;
&lt;p&gt;The scenario that bites teams most often: a Lambda that does meaningful work per invocation. Parsing a large payload, doing in-memory transformation, making multiple downstream calls.&lt;/p&gt;
&lt;p&gt;These functions run longer, use more memory, and the cost per invocation adds up faster than the pricing page suggests.&lt;/p&gt;
&lt;p&gt;A few numbers from real workloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A lightweight REST Lambda (Go, 128MB, 20ms avg): ~$0.40 per million requests&lt;/li&gt;
&lt;li&gt;A heavier processing Lambda (Node, 512MB, 800ms avg): ~$8.00 per million requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That 20× difference in cost per million requests adds up quickly when you have real traffic.&lt;/p&gt;
&lt;p&gt;It's still cheap compared to running servers — but the assumption that serverless is always the low-cost option needs to be verified, not assumed.&lt;/p&gt;
&lt;p&gt;API Gateway pricing is also frequently missed. At $3.50 per million requests for HTTP APIs, a high-traffic public endpoint can make Gateway cost more than the Lambda it fronts.&lt;/p&gt;
&lt;p&gt;Know the full cost model before you commit.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;debugging-is-a-different-skill&quot;&gt;Debugging is a different skill&lt;/h3&gt;
&lt;p&gt;Local development for Lambda is awkward.&lt;/p&gt;
&lt;p&gt;You can simulate the execution environment with tools like AWS SAM or custom test harnesses, but you're never truly running the same thing that runs in production.&lt;/p&gt;
&lt;p&gt;The gap shows up in exactly the moments you can least afford it.&lt;/p&gt;
&lt;p&gt;Observability requires deliberate setup.&lt;/p&gt;
&lt;p&gt;A Lambda that exits silently leaves no trace unless you've instrumented it. CloudWatch Logs are the floor, not the ceiling — you need structured logging, correlation IDs, and ideally X-Ray tracing before you're in production, not after something breaks.&lt;/p&gt;
&lt;p&gt;The failure modes are also harder to reproduce.&lt;/p&gt;
&lt;p&gt;A Cognito authorizer rejecting a token before your code runs looks different from your code returning a 401. A DynamoDB throttle on a hot partition looks different from a Lambda timeout.&lt;/p&gt;
&lt;p&gt;Getting fast at debugging serverless systems means learning the AWS console deeply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CloudWatch Insights queries&lt;/li&gt;
&lt;li&gt;X-Ray service maps&lt;/li&gt;
&lt;li&gt;reading IAM policy errors without a stack trace to guide you&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;statelessness-is-a-constraint-not-just-a-pattern&quot;&gt;Statelessness is a constraint, not just a pattern&lt;/h3&gt;
&lt;p&gt;Every Lambda invocation starts with a clean slate.&lt;/p&gt;
&lt;p&gt;That's the feature — it's what makes horizontal scaling effortless.&lt;/p&gt;
&lt;p&gt;It's also the constraint that eliminates entire categories of architectural approaches.&lt;/p&gt;
&lt;p&gt;No in-memory caches that persist across requests. No background jobs that run between invocations. No WebSocket connections that stay open. No files written to disk that the next request can read.&lt;/p&gt;
&lt;p&gt;If your application assumes any of those things, serverless isn't wrong — but it's not your foundation. It's a piece of a larger architecture that needs something else alongside it.&lt;/p&gt;
&lt;p&gt;This comes up most often when teams decide mid-project that they suddenly need a feature requiring persistent state.&lt;/p&gt;
&lt;p&gt;Adding ElastiCache or SQS to a serverless architecture is straightforward — but it's a different architecture than what you started with, and the integration work is non-trivial.&lt;/p&gt;
&lt;p&gt;The most dangerous part is that many of these problems don't show up until the system already matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;vendor-lock-in-is-real-and-that-s-okay&quot;&gt;Vendor lock-in is real, and that's okay&lt;/h3&gt;
&lt;p&gt;A Lambda function isn't a Docker container.&lt;/p&gt;
&lt;p&gt;It doesn't run the same way everywhere else.&lt;/p&gt;
&lt;p&gt;Your Go binary targeting &lt;code&gt;provided.al2023&lt;/code&gt; on arm64 can technically run in other environments, but your API Gateway routes, Cognito authorizers, DynamoDB single-table design, and SES integration are AWS-specific throughout.&lt;/p&gt;
&lt;p&gt;I'm not arguing against it — I use all of those services in production.&lt;/p&gt;
&lt;p&gt;But it's worth being honest:&lt;/p&gt;
&lt;p&gt;If you go deep on serverless AWS, migrating later means a substantial rewrite.&lt;/p&gt;
&lt;p&gt;Make that decision deliberately, not by default.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-not-to-use-it&quot;&gt;When not to use it&lt;/h3&gt;
&lt;p&gt;Serverless is the wrong tool when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have &lt;strong&gt;persistent connections&lt;/strong&gt; — WebSocket servers, gRPC streams, database connection pools&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;long-running jobs&lt;/strong&gt; — Lambda has a 15-minute max timeout&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;very high, sustained concurrency&lt;/strong&gt; — account-level concurrency limits can affect multiple services simultaneously&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;low and predictable latency&lt;/strong&gt; at P99 — cold-start tail latency is difficult to eliminate completely&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-it-s-the-right-call&quot;&gt;When it's the right call&lt;/h3&gt;
&lt;p&gt;Event-driven workloads.&lt;/p&gt;
&lt;p&gt;APIs with bursty or unpredictable traffic.&lt;/p&gt;
&lt;p&gt;Background processing triggered by S3 uploads, SQS messages, or DynamoDB streams.&lt;/p&gt;
&lt;p&gt;Any workload where you don't want to manage capacity and the per-request cost model fits your traffic profile.&lt;/p&gt;
&lt;p&gt;The API powering this site is Go Lambdas behind API Gateway.&lt;/p&gt;
&lt;p&gt;Contact form submissions, resume requests, visitor analytics, admin dashboard — all of it.&lt;/p&gt;
&lt;p&gt;It handles the traffic I need, costs less than $1/month to run, and I've never paged for a server that went down.&lt;/p&gt;
&lt;p&gt;For that workload, it's the right call.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The key is understanding what you're actually trading.&lt;/p&gt;
&lt;p&gt;Serverless isn't simpler than traditional infrastructure.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;It's differently complex.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The operational burden shifts from server management to observability, IAM, and distributed system debugging.&lt;/p&gt;
&lt;p&gt;That trade is absolutely worth it in the right context.&lt;/p&gt;
&lt;p&gt;Go in with your eyes open, and it's one of the most productive architectures available.&lt;/p&gt;
&lt;p&gt;The right architecture should help your team maintain momentum as the system grows — not create friction six months later.&lt;/p&gt;
&lt;p&gt;At M²S² Engineering Group, we help teams make these decisions before they become expensive to unwind.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://m2s2.io/blog/serverless-aws-nobody-mentions&quot;&gt;m2s2.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19091/the-truth-about-serverless-apis-nobody-mentions</guid>
<pubDate>Wed, 27 May 2026 15:51:56 +0000</pubDate>
</item>
<item>
<title>Multi-Cloud Resilience: The Event-Driven Cellular Architecture Blueprint across AWS and Azure</title>
<link>https://coderlegion.com/16402/multi-cloud-resilience-the-event-driven-cellular-architecture-blueprint-across-aws-azure</link>
<description>&lt;h5 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h5&gt;
&lt;p&gt;As enterprise systems reach massive scale, relying on a single cloud provider introduces systemic risk. Regional outages, vendor-specific API degradations, or severe misconfigurations can result in global downtime. Multi-cloud cellular architecture solves this by deploying isolated, self-contained failure domains (cells) across different cloud providers. In this tutorial, you will architect a unified, event-driven system where AWS and Azure serve as the physical hosts for identical logical cells. &lt;/p&gt;
&lt;p&gt;Instead of building a &quot;stretched&quot; application that queries data across clouds—an anti-pattern that guarantees high latency and fragile dependencies—you will deploy complete, asynchronous processing pipelines on both AWS and Azure. A global, cloud-agnostic edge routing layer will inspect incoming traffic and forward it to either an AWS cell or an Azure cell based on a partition key (such as &lt;code&gt;TenantId&lt;/code&gt;). By the end of this guide, you will understand how to orchestrate this ultimate fault-isolation boundary using Terraform, ensuring your application survives even a total cloud provider outage.&lt;/p&gt;
&lt;h5 id=&quot;2-prerequisites&quot;&gt;2. Prerequisites&lt;/h5&gt;
&lt;p&gt;To build a multi-cloud environment, your operational tooling must be strictly cloud-agnostic. You will need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Active accounts on both Amazon Web Services (AWS) and Microsoft Azure with administrative privileges.&lt;/li&gt;
&lt;li&gt;The AWS CLI and Azure CLI installed and authenticated on your local machine.&lt;/li&gt;
&lt;li&gt;Terraform (version 1.3.0 or higher) installed. Terraform is crucial here, as it is the singular control plane capable of deploying resources to both clouds simultaneously.&lt;/li&gt;
&lt;li&gt;Understanding of Global Server Load Balancing (GSLB) or Edge Compute platforms (like Cloudflare Workers or Fastly) to act as the multi-cloud router.&lt;/li&gt;
&lt;li&gt;A Domain-Driven Design (DDD) approach to your data, ensuring that a specific tenant's data lives entirely within one cell and never needs to cross the multi-cloud boundary.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;3-step-by-step&quot;&gt;3. Step-by-Step&lt;/h5&gt;
&lt;h6 id=&quot;step-1-configuring-multi-cloud-providers-in-terraform&quot;&gt;Step 1: Configuring Multi-Cloud Providers in Terraform&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Create a root Terraform configuration that initializes providers for both AWS and Azure simultaneously. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Infrastructure as Code (IaC) is the only sustainable way to manage multi-cloud environments. By declaring both providers in the same root module, you can orchestrate the deployment of AWS cells, Azure cells, and the DNS records that tie them together from a single pipeline, preventing configuration drift between your cloud environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code Example (&lt;code&gt;main.tf&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;terraform {
  required_version = &quot;&amp;gt;= 1.3.0&quot;
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;~&amp;gt; 5.0&quot;
    }
    azurerm = {
      source  = &quot;hashicorp/azurerm&quot;
      version = &quot;~&amp;gt; 3.0&quot;
    }
  }
}

provider &quot;aws&quot; {
  region = &quot;us-east-1&quot;
}

provider &quot;azurerm&quot; {
  features {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6 id=&quot;step-2-deploying-the-aws-and-azure-data-plane-cells&quot;&gt;Step 2: Deploying the AWS and Azure Data Plane Cells&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Utilize the modular approach to instantiate independent cells on both clouds. The AWS cell utilizes DynamoDB, SNS, and SQS. The Azure cell utilizes Cosmos DB, Service Bus Topics, and Subscriptions. Both rely on serverless compute (Lambda and Azure Functions).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; While the underlying managed services differ, the architectural pattern is identical: an HTTP entry point writes to a NoSQL database, which triggers a change stream, which is fanned out via a message bus to consumer workers. Encapsulating these into Terraform modules abstracts the cloud-specific complexities, allowing you to treat &quot;AWS&quot; and &quot;Azure&quot; simply as deployment targets for your business logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code Example (&lt;code&gt;data_plane.tf&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# AWS Cell (East Coast)
module &quot;cell_aws_alpha&quot; {
  source  = &quot;./modules/aws_event_cell&quot;
  cell_id = &quot;aws-alpha&quot;
  region  = &quot;us-east-1&quot;
}

# Azure Cell (East Coast)
module &quot;cell_azure_beta&quot; {
  source   = &quot;./modules/azure_event_cell&quot;
  cell_id  = &quot;azure-beta&quot;
  location = &quot;East US&quot;
}

# Azure Cell (Europe)
module &quot;cell_azure_gamma&quot; {
  source   = &quot;./modules/azure_event_cell&quot;
  cell_id  = &quot;azure-gamma&quot;
  location = &quot;North Europe&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6 id=&quot;step-3-implementing-the-cloud-agnostic-edge-router&quot;&gt;Step 3: Implementing the Cloud-Agnostic Edge Router&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Deploy a global routing layer that sits entirely outside of AWS and Azure. This is typically achieved using an Edge Compute platform like Cloudflare Workers. The Worker intercepts the HTTP request, reads the &lt;code&gt;TenantId&lt;/code&gt;, checks a globally distributed Key-Value store to find the assigned cell's API endpoint, and forwards the payload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; If your global router is hosted on AWS (e.g., using API Gateway), and AWS experiences an outage, your Azure cells become unreachable, defeating the purpose of multi-cloud. The routing layer must be cloud-agnostic. Cloudflare Workers execute at the edge, providing sub-millisecond lookups for the routing map and directing traffic to the respective AWS API Gateway or Azure Function HTTP trigger.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conceptual Edge Router Logic (JavaScript for Edge Worker):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const tenantId = request.headers.get(&quot;X-Tenant-ID&quot;);

    if (!tenantId) {
      return new Response(&quot;Missing Tenant ID&quot;, { status: 400 });
    }

    // Lookup cell assignment from global edge KV store
    const cellEndpoint = await env.ROUTING_MAP.get(tenantId);

    if (!cellEndpoint) {
      return new Response(&quot;Tenant mapping not found&quot;, { status: 404 });
    }

    // Proxy the request to the target cloud (AWS or Azure)
    const targetUrl = new URL(url.pathname, cellEndpoint);
    const modifiedRequest = new Request(targetUrl, request);
    
    return fetch(modifiedRequest);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6 id=&quot;step-4-automating-disaster-recovery-and-traffic-shifting&quot;&gt;Step 4: Automating Disaster Recovery and Traffic Shifting&lt;/h6&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Establish a process to update the global routing map. If the AWS region hosting &lt;code&gt;cell_aws_alpha&lt;/code&gt; goes offline, you update the Edge KV store to point those affected tenants to a standby cell on Azure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Cellular architecture drastically reduces the blast radius of an outage, but you still need a mechanism to restore service for the degraded cell. By decoupling the routing logic from the compute infrastructure, shifting traffic between clouds becomes a simple Key-Value update at the DNS/Edge layer, resulting in near-instantaneous failover.&lt;/p&gt;
&lt;h5 id=&quot;4-common-troubleshooting&quot;&gt;4. Common Troubleshooting&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Multi-Cloud Data Split Brain:&lt;/strong&gt; The biggest mistake in multi-cloud architecture is attempting synchronous database replication between AWS and Azure. This introduces severe latency and massive egress costs. Ensure strict cellular isolation: a tenant's data must live and be processed entirely within their assigned cell. Data should only cross clouds during a deliberate, asynchronous disaster recovery migration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD Pipeline Complexity:&lt;/strong&gt; Deploying to multiple clouds means your deployment pipelines must authenticate with two different IAM systems. Utilize OpenID Connect (OIDC) in platforms like GitHub Actions or GitLab CI. OIDC allows your pipelines to assume temporary, secure roles in both AWS (via IAM Identity Center) and Azure (via Microsoft Entra Workload ID) without storing long-lived, vulnerable access keys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability Fragmentation:&lt;/strong&gt; AWS logs live in CloudWatch; Azure logs live in Log Analytics. Troubleshooting a multi-cloud environment requires a unified view. You must export logs and metrics from both cloud providers into a centralized, agnostic observability platform (like Datadog, New Relic, or a self-hosted ELK stack) to effectively monitor the health of your global cells.&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id=&quot;5-conclusion&quot;&gt;5. Conclusion&lt;/h5&gt;
&lt;p&gt;Building an event-driven cellular architecture across multiple clouds is the pinnacle of infrastructure resilience. By treating AWS and Azure as interchangeable hosts for isolated data planes, and governing them with a cloud-agnostic edge router and Terraform, you eliminate single points of failure at the vendor level. While this approach introduces operational complexity, it guarantees that a regional outage or provider degradation remains a contained incident rather than a business-ending catastrophe. As a next step, focus on standardizing your application packaging by utilizing containers (ECS on AWS, Container Apps on Azure) to ensure your worker code executes identically regardless of the host cloud.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16402/multi-cloud-resilience-the-event-driven-cellular-architecture-blueprint-across-aws-azure</guid>
<pubDate>Sat, 02 May 2026 12:42:02 +0000</pubDate>
</item>
</channel>
</rss>