The hidden cost of Promise In NodeJS

Panth PatelPanth Patel
5 min read

I was just watching some videos, you know, YT and chill.
https://www.youtube.com/watch?v=i0YfiQlzv6M
Then Prime mentioned something that caught me off guard. He said sync Promises go behind event loops. I was like, no way, that can't be true. Promises have to be wrapped in function wrappers to work, and they have built-in syntax. NodeJS wouldn't take shortcuts like that, right? RIGHT?!

async function a() {
    console.log('a');
}
console.log('1');
setTimeout(() => console.log('t1')); // this gose to next event loop
a(); // this is sync code, so should not go to event loop
setTimeout(() => console.log('t2'));
console.log('2');

Here is the output

1
a
2
t1
t2

Yes, I was right.

But hold on, this might just be due to the async syntax. Maybe the Promise constructor works differently. Perhaps .then and await actually go to the event loop. Let's find out.

async function a() {
    console.log('a1');
    const p = new Promise((r) => {
       console.log('p>>>')
       r();
    }).then(() => console.log('p<<<'));
    console.log('a2');
    await p;
    console.log('a3');
}
console.log('1');
setTimeout(() => console.log('t1'));
a();
setTimeout(() => console.log('t2'));
console.log('2');

Here is the output

1
a1
p>>>
a2
2
p<<<
a3
t1
t2

OMG, he's right! The log p<<< should come before 2, which proves that async returns are pushed to the next event loop tick!
If you're not sure what this means 😰, it's a big deal.
Let me show you with an example.

Let's make some empty sync and async functions, and we'll call them a million times.

  1. Use a sync function and run it synchronously.

  2. Use a sync function and run it asynchronously.

  3. Use an async function and run it synchronously.

  4. Use an async function and run it asynchronously.

  5. Use an async function, run it synchronously, and await all at once.

function sf() {}
async function af() {}
function run(fn, next) {
  const start = Date.now();
  function done() {
    console.log(Date.now() - start + "ms");
    if (next) next();
  }
  fn(done);
}
function case1() {
  run((done) => {
    for (let index = 0; index < 1000_000; index++) {
      sf();
    }
    done();
  }, case2);
}
function case2() {
  run(async (done) => {
    for (let index = 0; index < 1000_000; index++) {
      await sf();
    }
    done();
  }, case3);
}
function case3() {
  run((done) => {
    for (let index = 0; index < 1000_000; index++) {
      af();
    }
    done();
  }, case4);
}
function case4() {
  run(async (done) => {
    for (let index = 0; index < 1000_000; index++) {
      await af();
    }
    done();
  }, case5);
}
function case5() {
  run(async (done) => {
    const promises = [];
    for (let index = 0; index < 1000_000; index++) {
      promises.push(af());
    }
    await Promise.all(promises);
    done();
  });
}
case1();
sync func & sync exesync func & async exeasync func & sync exeasync func & async exeasync func & await all exe
Crome Browser3ms1500ms33ms1559ms
NodeJS2ms51ms7ms46ms171ms
Deno1ms49ms7ms42ms183ms
Bun2ms73ms19ms74ms130ms
Crome Browser Fresh Start3ms1289ms33ms1477ms388ms

😱😱 OH NO 😱😱

None of these runtimes have fixed this, and the browser is the slowest of them all, probably because I have way too many tabs open (Nope).

Let's try doing something inside our empty functions, just in case these runtimes have a little optimization for empty functions.

let cnt = 0;
function sf() {
  cnt++;
}
async function af() {
  cnt++;
}
function run(fn, next) {
  const start = Date.now();
  function done() {
    cnt = 0;
    console.log(Date.now() - start + "ms");
    if (next) next();
  }
  fn(done);
}
sync func & sync exesync func & async exeasync func & sync exeasync func & async exeasync func & await all exe
Crome Browser Fresh Start3ms1307ms32ms1493ms396ms
NodeJS10ms50ms8ms46ms170ms
Deno5ms51ms7ms44ms181ms
Bun4ms68ms20ms76ms131ms

DAMN

I really did not know cost of this, Now I know.

Now I get why folks using Rust and GoLang say async is tough when I thought it was easy. Sure, it's easy, but at what cost? If you want to build high-performance software, you really have to lean on sync callbacks. Async will just slow your code down by 10 times, and for what? Literally doing nothing. It's wild. Node JS is super popular, and yet this is where we're at!

Damn!!

I’m not sure what to do next, but I guess I’ll need to dig deeper to figure out the best approach. Running any sync code in async is clearly a big bottleneck. Maybe I’ll revamp my libraries and framework to fully support sync callbacks, and who knows, I might even rewrite the backend from scratch to make the most out of callbacks.

0
Subscribe to my newsletter

Read articles from Panth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Panth Patel
Panth Patel