AWS Builder Center

Lambda vs. Step Functions Execution Time

Lambda is obviously faster than StepFunctions, right? Well, maybe. Quickly poking around in CloudWatch Metrics shows it isn't necessarily clear cut.

Founder at Proxylity
While perusing metrics in CloudWatch I came across something a little surprising that might be worth consideration when choosing between Lambda and Step Functions for simple algorithmic tasks.

Some Background

To help people understand how to use our service, we publish a repository of examples. Some are production-ready, and some are simple and just illustrate the basic usage an options. One of them is the "packet counter" which has implementations using both Step Functions and Lambda (multiple code Languages).
The logic itself is about as simple as it gets: count the number of items (packets) in an array, grouped by a key (source IP address). Nothing fancy, no downstream calls. It does the simple algorithmic work that often comes into play, and is (roughly) the basis for a fair comparison between the services.
On the Step Functions side, there is a single Pass state with a JSONata expression doing the counting. No Lambda invocation inside it, no extra states. Just one node; almost the simplest possible state machine.
plaintext
{% (
$counts := $states.input.Messages.Remote{`IpAddress`: $count($)};
$replies := $each($counts, function($v, $i) { ({
"Tag": $filter($states.input.Messages, function($m) { ($m.Remote.IpAddress = $i ) })[0].Tag,
"Data": $base64encode($string($v))
}) });
{
"Replies": [$replies]
}
) %}
On the Lambda side, it's a Node.js function doing the same count.
JavaScript
exports.handler = async (inbound_packets) => {
console.log("Received packets:", JSON.stringify(inbound_packets, null, 2));
const counts_by_src = inbound_packets.Messages.reduce((acc, obj) => {
const src = obj.Remote.IpAddress;
acc[src] = (acc[src] || 0) + 1;
return acc;
}, {})
function get_and_clear_count_in_b64(map, key) {
const value = map[key];
if (value == null) return null;
map[key] = null;
return btoa(value.toString() + ' us-west-2\n');
}
const outbound_packets = inbound_packets.Messages.map(function (p) {
const d = get_and_clear_count_in_b64(counts_by_src, p.Remote.IpAddress);
if (d == null) return null;
return {
GeneratedAt: new Date().toISOString(),
Tag: p.Tag, Data: d
}
});
return { Replies: outbound_packets };
};
I use these particular examples inconsistently, mainly for demos and training, but they get executed enough to generate interesting statistics over a long time period.

The Metrics

I first grabbed execution duration for the Lambda and state machine (for whatever reason they're named sightly differently, "Duration" for Lambda and "Execution duration" for Step Functions) over about 24 hours, looking at p99 and p99.9 at 1-hour granularity/periods.
Lambda's p99 if fairly consistent and ranges between about 5ms and 10ms throughout the day. By comparison the p99.9 varies a bit more, from about 9ms to 25ms. So 2-2.5x difference:
One day of Lambda execution duration metrics, with 1-hour granularity.

Step Functions' p99 sits a bit higher, but the p99.9 is largely much lower (with a couple spikes):
One day of Step Functions execution duration, with 1-hour granularity.

This seems to indicate that Step Functions is much more consistent and perhaps a better choice when tail latency really matters.
I found this a little odd and decided to "zoom out" and look at the same metrics over six months at a daily granularity (my thinking was that the hourly granularity didn't contain enough datapoints to be significant):
Six months of the Lambda execution duration metrics, with 1-day granularity.

Six months of execution duration data for the State machine, with 1-day granularity.

This data is more sporadic since the stacks aren't always in use, but seems to confirm that Step Functions does indeed exhibit a significantly lower p99.9 execution duration. But wow, it also contains another surprises: the Step Functions execution duration got much slower over the past months! It was even faster before February. I have no idea why that would be.
Has anyone else seen this? Any theories on why? Maybe degree of use?

What I'm taking from this, tentatively

I don't think this changes my default. For SDK calls, JSON transforms and simple algorithmic work a Step Functions state machine is still what I reach for first. The deployment speed, lack of runtime and dependency updates and lack of cold start are too much of a benefit to go to Lambda for those tasks.
But now I have (maybe?) another reason: more predictable p99.9 execution time (tail latency). if you're in a spot where the rare bad outcome is the thing you actually get judged on, it might be worth pulling the p99.9 numbers before assuming Lambda wins automatically.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article