Eliminating JavaScript cold starts on AWS Lambda

2025-08-1611:4420069goose.icu

Porffor can run on Lambda now!

Porffor is my JS engine/runtime that compiles JavaScript ahead-of-time to WebAssembly and native binaries. What does that actually mean? You can compile JS files to tiny (<1MB), fast (millisecond-level) binaries:

~$ bat hi.js
─────┬──────────────────────────────────────
 1 │ console.log("hello blog!")
─────┴──────────────────────────────────────
~$ porf native hi.js hi
[271ms] compiled hi.js -> hi (12.9KB)
~$ du -h hi
16K hi
~$ ./hi
hello blog!

Node and Bun offer “compile” options, but they bundle their runtime with your JS rather than actually compiling it as if it was C++ or Rust. Porffor does that, allowing for much smaller and faster binaries:

~$ deno compile -o hi_deno hi.js
~$ bun build --compile --outfile=hi_bun hi.js
~$ du -h hi*
16K hi
97M hi_bun
82M hi_deno
4.0K hi.js
~$ hyperfine -N "./hi" "./hi_deno" "./hi_bun" --warmup 5
Benchmark 1: ./hi
 Time (mean ± σ): 631.4 µs ± 128.5 µs [User: 294.5 µs, System: 253.1 µs]
 Range (min … max): 465.3 µs … 1701.3 µs 2762 runs

Benchmark 2: ./hi_deno
 Time (mean ± σ): 37.4 ms ± 1.7 ms [User: 22.5 ms, System: 16.0 ms]
 Range (min … max): 33.8 ms … 42.2 ms 74 runs

Benchmark 3: ./hi_bun
 Time (mean ± σ): 15.9 ms ± 1.2 ms [User: 8.7 ms, System: 9.6 ms]
 Range (min … max): 13.7 ms … 19.2 ms 175 runs

Summary
 ./hi ran
 25.24 ± 5.50 times faster than ./hi_bun
 59.30 ± 12.36 times faster than ./hi_deno

What’s the trade-off? You have to re-invent the JS engine (and runtime) so it is still very early: limited JS support (but over 60% there) and currently no good I/O or Node compat (yet). But, we can use these tiny fast native binaries on Lambda!


Lambda

A few days ago I got Porffor running on Lambda, not simulated locally but really on AWS! I wrote a cold start benchmark for Node, LLRT (Amazon’s own experimental JS runtime optimizing cold starts) and Porffor running identical code:

export const handler = async () => {
 return {
 statusCode: 200,
 headers: { "Content-Type": "text/plain" },
 body: "Hello from " + navigator.userAgent + " at " + Date()
 };
};

Since we’re benchmarking cold start, the workload does not matter as we are interested in just how we are running here (for context most Lambdas run for <1s, typically <500ms). I spent over a day just benchmarking and even with my biases, the results surprised me.

Node

A graph of benchmark results for Node, explained below

Here is Node (managed, nodejs22.x), our main comparison and baseline. Surprisingly alright, but still far from ideal: having your users have to wait up to 0.3s due to a technical limitation out of your control just sucks.

We don’t prioritize memory usage here, as AWS bills based on allocated memory rather than actual usage. In this benchmark, we allocate the minimum (128MB), ensuring it remains below that threshold. I’ll show the cost in GB-seconds, calculated as billed duration (from AWS) multiplied by allocated memory.

Also, Node is a managed runtime, meaning AWS supplies it for you. This significantly aids with initialization duration by allowing for effective caching. Crucially, we are not billed for this init duration, which profoundly impacts cost. (While an AWS blog post indicates that this will change starting August 1st, this data is from August and does not yet reflect such charges. I will update if this changes.)

LLRT

A graph of benchmark results for LLRT, explained below

LLRT is ~3x faster than Node here, great! Unfortunately in my testing, it also costs ~1.6x more than Node. This is only due to the managed runtime trick explained before. This should change when they charge for that init or create a managed runtime once LLRT is stable. Overall, ignoring that hitch, much better than Node for this benchmark!

Porffor

A graph of benchmark results for Porffor, explained below

Porffor is ~12x faster than Node and almost 4x faster than LLRT in this case. Plus, even with Node’s managed runtime trick, it is over 2x cheaper than Node (and almost 4x cheaper than LLRT). 🫳🎤 I hope this shows that when Porffor works, it works extremely well: Porffor’s P99 is faster than both LLRT’s and Node’s P50.


Conclusion

You might be expecting me to start shilling for you to plug Porffor into your Lambda instantly… but unfortunately not. Porffor is still very (pre-alpha) early.

Although, if you/your company have small Lambdas (ideally no Node APIs) and want a free quick look for if Porffor could help you, please email me! Porffor is actively improving and more code is working everyday.

For full transparency: benchmark code, CSV data and graphs are available on GitHub here.


Read the original article

Comments

  • By samwillis 2025-08-1712:433 reply

    Oliver is doing awesome work here. A few interesting points:

    - Porffor can use typescript types to significantly improve the compilation. It's in many ways more exciting as a TS compiler.

    - There's no GC yet, and likely will be a while before it gets any. But you can get very far with no GC, particularly if you are doing something like serving web requests. You can fork a process per request and throw it away each time reclaiming all memory, or have a very simple arena allocator that works at the request level. It would be incredibly performant and not have the overhead of a full GC implementation.

    - many of the restrictions that people associate with JS are due to VMs being designed to run untrusted code. If you compile your trusted TS/JS to native you can do many new things, such as use traditional threads, fork, and have proper low level memory access. Separating the concept of TS/JS from the runtime is long overdue.

    - using WASM as the IR (intermediate representation) is inspired. It is unlikely that many people would run something compiled with Porffor in a WASM runtime, but the portability it brings is very compelling.

    This experiment from Oliver doesn't show that Porffor is ready for production, but it does validate that he is on the right track, and that the ideas he is exploring are correct. That's the imports take away. Give it 12 months and exciting things will be happing.

    • By spankalee 2025-08-1717:461 reply

      I'm very excited by Porffor too, but a lot of what you've said here isn't correct.

      > - Porffor can use typescript types to significantly improve the compilation. It's in many ways more exciting as a TS compiler.

      Proffor could use types, but TypeScript's type system is very unsound and doing so could lead to serious bugs and security vulnerabilities. I haven't kept track of what Oliver's doing here lately, but I think the best and still safe thing you could do is compile an optimistic, optimized version of functions (and maybe basic blocks) based on the declared argument types, but you'd still need a type guard to fall back to the general version when the types aren't as expected.

      This isn't far from what a multi-tier JIT does, and the JIT has a lot more flexibility to generate functions for the actual observed types, not just the declared types. This can be a big help when the declared types are interfaces, but in an execution you only see specific concrete types.

      > or have a very simple arena allocator that works at the request level.

      This isn't viable. JS semantics mean that the request handling path can generate objects that are held from outside the request's arena. You can't free them or you'd get use-after-free problems.

      > - many of the restrictions that people associate with JS are due to VMs being designed to run untrusted code

      This is true to some extent, but most of the restrictions are baked into the language design. JS is a single-threaded non-shared memory language by design. The lack of threads has nothing to do with security. Other sandboxed languages, famously Java, have threads. Apple experimented with multithreaded JS and it hasn't moved forward not because of security but because it breaks JS semantics. Fork is possible in JS already, because it's a VM concept, not a language concept. Low-level memory access would completely break the memory model of JS and open up even trusted code to serious bugs and security vulnerabilities.

      > It is unlikely that many people would run something compiled with Porffor in a WASM runtime

      Running JS in WASM is actually the thing I'm most excited about from Porffor. There are a more and more WASM runtimes, and JS is handicapped there compared to Rust. Being able to intermix JS, Rust, and Go in a single portable, secure runtime is a killer feature.

      • By samwillis 2025-08-1718:061 reply

        > I haven't kept track of what Oliver's doing here lately

        Please do go and check up what the state of using types to inform the compiler is (I'm not incorrect)

        On the area allocator, I wasn't clear enough, as stated elsewhere this was in relation to having something similar to isolates - each having a memory space that's cleaned up on exit.

        Python has almost identical semantics to JS, and has threads - there is nothing in the EMCAScript standard that would prevent them.

        • By spankalee 2025-08-1718:481 reply

          It is absolutely true that it is unsafe to trust TypeScript types. I've chatted briefly with Oliver on socials before and he knows this. So I am a bit confused by this issue: https://github.com/CanadaHonk/porffor/issues/234 which says "presume the types are good and have been validated by the user before compiling". This is just not a thing that's possible. Types are often wrong in subtle ways. Casts throw everything out the window.

          Dart had very similar issues and constraints and they couldn't do a proper AOT compiler that considered types until they made the type system sound. TypeScript can never do that and maintain compatibility with JS.

          Isolates are already available as workers. The key thing is that you can't have shared memory, other wise you can get cross-Isolate references and have all the synchronization problems of threads.

          And ECMAScript is simply just specified as a single-threaded language. You break it with shared-memory threads.

          In JS, this always logs '4'. With threads that's not always the case.

              let x = 4;
              console.log(x);

          • By nicoburns 2025-08-187:02

            > It is absolutely true that it is unsafe to trust TypeScript types... This is just not a thing that's possible.

            Well... unsafe and impossible aren't quite the same thing. I guess this is possible if you throw out "safe" as a requirement?

    • By gibolt 2025-08-1714:482 reply

      Based on how much imported libraries are relied upon, it makes sense to treat everything as untrusted. Unless you write every line yourself/in-house, code should be considered untrusted.

      I would be curious which attack vectors change or become safe after compiling though.

      • By samwillis 2025-08-1715:061 reply

        The point of the js engine sandbox is to protect the user in the browser - it's completely redundant on the server. Supply chain attacks are real, but only Deno has tried to fix that through permissions/rules.

        I don't think anything changes with compile to native on the server.

        • By rafram 2025-08-1716:17

          Totally disagree. A spec-compliant JS engine has to support the features that allow vulnerabilities like prototype pollution, which can be exploited through user input alone.

      • By hinkley 2025-08-1716:21

        Also none of the third party code will be thread safe. Hell, some of it isn’t even reentrant.

    • By bastawhiz 2025-08-1714:453 reply

      > many of the restrictions that people associate with JS are due to VMs being designed to run untrusted code. If you compile your trusted TS/JS to native you can do many new things, such as use traditional threads, fork, and have proper low level memory access. Separating the concept of TS/JS from the runtime is long overdue.

      This is just outright wrong. JS limitations come from lots of things:

      1. The language has almost zero undefined behavior by design. Code will essentially never behave differently on different platforms.

      2. JS has traditional threads in the form of web workers. This interface exists not for untrusted code but because of thread safety. That's a language design, like channels in Go, rather than a sandboxing consideration.

      3. Pretty much every non-browser JS runtime has the ability to fork.

      4. JS is fully garbage collected, of course you don't get your own memory management. You can use buffers to manage your own memory if you really want to. WASM lets you manage your own memory and it can run "untrusted" code in the browser with the WASM runtime; your example just doesn't hold water. There's no way you could fiddle with the stack or heap in JS without making it not JS.

      5. The language comes with thirty years of baggage, and the language spec almost never breaks backwards compatibility.

      Ironically Porffor has no IO at the moment, which is present in literally every JS runtime. It really has nothing to do with untrusted code like you're suggesting.

      > You can fork a process per request and throw it away each time reclaiming all memory, or have a very simple arena allocator that works at the request level. It would be incredibly performant and not have the overhead of a full GC implementation.

      You also must admit that this would make Porffor incompatible with existing runtimes. Code today can modify the global state, and that state can and does persist across requests. It's a common pattern to keep in-memory caches or to lazily initialize libraries. If every request is fully isolated in the future but not now, you can end up with performance cliffs or a system where a series of requests on Node return different results than a series of requests on Porffor.

      As for arena allocation, this makes it even less compatible with Node (if not intractable). If means you can't write (in JS) any code that mutates memory that was initialized during startup. If you store a reference to an object in an arena in an object initialized during startup, at the end of the request when the arena is freed you now have a pointer into uninitialized memory.

      How do you tell the developer what they can and cannot mutate? You can't, because any existing variable might be a reference to memory initialized during startup. Your function might receive an object as an argument that was initialized during startup or one that's wasn't, and there's no way to know whether it's safe to mutate it.

      Long story short, JS must have a garage collector to free memory, or it's not JS.

      > It is unlikely that many people would run something compiled with Porffor in a WASM runtime, but the portability it brings is very compelling.

      Node (via SEA in v20), bun, and deno all have built in tooling for generating a self-contained binary. Granted, the runtime needs to work for your OS and CPU, but the exact same thing could be said about a WASM runtime.

      And of course there are hundreds of mature bundlers that can compile JS into a single file that runs in various runtimes without ever thinking about platform. It's weird to even consider portability of JS as a benefit because JS is already almost maximally portable.

      > This experiment from Oliver doesn't show that Porffor is ready for production, but it does validate that he is on the right track, and that the ideas he is exploring are correct.

      It validates that the approach to building a compiler is correct, but it says little about whether the project will eventually be usable and good. It's unlikely it'll get faster, because robust JS compatibility will require more edge cases to be handled than it currently does, and as Porffor's own README says, it's still slower than most JITted runtimes. A stable release might not yield much.

      • By cxr 2025-08-1715:22

        What a strange (and strangely adversarial) comment.

        Almost none of your criticisms connects with anything that the other person wrote.

      • By hinkley 2025-08-1716:23

        > JS has traditional threads in the form of web workers.

        There is no language I’m aware of where workers behave like “traditional threads”. They’re isolates. Not threads.

      • By samwillis 2025-08-1715:242 reply

        Web workers don't share memory (other than SAB) with the main thread, they are far from traditional threads. These APIs are designed the way they are to protect end users, stop sites from consuming resources or bad code blocking the main thread. None of that is needed to be that way on the server. There is zero reason that a JS implementation cannot implement proper threads within the same memory space. The issue is that all js engines are derived from the browser where that isn't wanted, they simply don't have support for it. Traditional threads need careful use,

        Nowhere did I say that full, or even any, compatibility with Node is needed - it isn't.

        We need to stop conflating JS the language with the runtimes.

        A JS runtime absolutely can get by without a GC, you just never dealloc and consume indefinitely. That doesn't change any semantics of the language, if a value/object is inaccessible, it's inaccessible...

        An arena allocator provides a route to say embedding a js-to-native app in a single threaded web server like Nginx, you don't need to share memory between what in effect become "isolates".

        • By no_wizard 2025-08-1718:531 reply

          NodeJS has worker threads[0] already

          [0]: https://nodejs.org/docs/latest/api/worker_threads.html

          • By crabmusket 2025-08-1722:10

            These are very similar to web workers, they don't share memory other than via SharedArrayBuffer instances. For anything else you use message passing.

        • By bastawhiz 2025-08-1716:461 reply

          > Web workers don't share memory (other than SAB) with the main thread, they are far from traditional threads. These APIs are designed the way they are to protect end users, stop sites from consuming resources or bad code blocking the main thread. None of that is needed to be that way on the server.

          It doesn't protect end users any more than it protects servers. Node could easily expose raw threading, but they don't because nearly the whole language isn't thread safe and everything would break. It has almost nothing to do with protecting users, it's a language design decision that enforces other design constraints.

          > We need to stop conflating JS the language with the runtimes

          If you're just sharing syntax but the standard library is different and essentially none of the code is compatible, it's not the same language. ECMAScript specifies all of the things you're talking about, and that is JavaScript, irrespective of the runtime.

          > A JS runtime absolutely can get by without a GC, you just never dealloc and consume indefinitely. That doesn't change any semantics of the language, if a value/object is inaccessible, it's inaccessible...

          If you throw away the whole heap on every request, now every request it's definitionally a "cold start". Which negates the singular benefit that this post is calling out. Porffor is still not faster than JITed engines at runtime, and initializing the code still has to happen.

          > Nowhere did I say that full, or even any, compatibility with Node is needed - it isn't.

          You have to square what you're saying with this statement. What you're describing is JavaScript in syntax only. You're talking about major departures from the formal language spec. Existing JavaScript code is likely to break. Why not just make a new language and call it something else, like Crystal is to Ruby? It works different, you're saying it doesn't care about compatibility... Why even call it JS then?

          • By samwillis 2025-08-1717:21

            > ECMAScript specifies all of the things you're talking about, and that is JavaScript, irrespective of the runtime.

            I suggest you go and read the EMCAScript standard: https://ecma-international.org/publications-and-standards/st...

            There is nothing in there about browser APIs, and in fact it explicitly states that the browser runtime, or any other runtime + api are not EMCAScript.

  • By unfunco 2025-08-1712:121 reply

    I really don't struggle that much with cold starts on Node.js/Lambda, and I don't do anything special, my build commands look like:

        esbuild src/handler.ts --bundle --external:@aws-lambda-powertools --external:@aws-sdk --minify --outfile=dist/handler.js --platform=node --sourcemap --target=es2022 --tree-shaking=true
    
    Maybe I'm not doing as much as others in my functions and I tend to stick within the AWS ecosystem, so I save some space and I presume cold-start time by not including the AWS SDK/Powertools in the output, but my functions tend to cold start and complete in ~100ms.

    • By anon7000 2025-08-1719:46

      Sure, but the approach mentioned here benchmarks with median performance of 16ms. 100ms isn’t great especially if it’s only one part of everything that needs to happen

  • By tkcranny 2025-08-1710:531 reply

    It’s a TS/JS to wasm to C tool chain, that runs the same JS a dozen times faster than on node. Very cool approach, and lambda cold starts are definitely where it ought to shine.

    That said I wonder if it could ever go mainstream – JS is not a trivial language anymore. Matching all of its quirks to the point of being stable seems like a monstrous task. And then Node and all of its APIs are the gorilla in the room too. Even Deno had to acquiesce and replicate those with bugs and all, and it’s just based on V8 too.

    • By tombh 2025-08-1711:25

      Hopefully Oliver, the creator of the project, is here to better answer, but something worth bearing in mind is that a significant part of conventional JS engines are all JIT tricks. And Porffor is AOT, so can lean on WASM and the C compiler for compilation optimisations. The author did a quick comparison of lines of code for some of the popular JS engines: https://goose.icu/js-engine-sizes

HackerNews