Skip to content

ffi: nextTick and microtasks queued in event-loop-invoked callbacks are not drained #66917

Open

Description

@trivikr

Version

main

Platform

macOS

Subsystem

ffi

What steps will reproduce the bug?

This uses libuv's own timer through dlopen(null), so native code calls the callback straight from the event loop:

import { dlopen, getCurrentEventLoop } from 'node:ffi';

const { lib, functions: uv } = dlopen(null, {
  uv_handle_size: { arguments: ['i32'], return: 'u64' },
  uv_timer_init: { arguments: ['pointer', 'pointer'], return: 'i32' },
  uv_timer_start: {
    arguments: ['pointer', 'pointer', 'u64', 'u64'],
    return: 'i32',
  },
});

const UV_TIMER = 13;
const timer = new Uint8Array(Number(uv.uv_handle_size(UV_TIMER)) + 64);
const start = performance.now();
const t = () => `${Math.round(performance.now() - start)}ms`;

const cb = lib.registerCallback({ arguments: ['pointer'], return: 'void' }, () => {
  console.log(`${t()} native timer callback`);
  process.nextTick(() => console.log(`${t()} nextTick`));
  queueMicrotask(() => console.log(`${t()} microtask`));
  Promise.resolve().then(() => console.log(`${t()} promise.then`));
});

uv.uv_timer_init(getCurrentEventLoop(), timer);
uv.uv_timer_start(timer, cb, 10n, 0n);

setTimeout(() => console.log(`${t()} unrelated setTimeout`), 1000);

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

3ms native timer callback
3ms nextTick
3ms microtask
5ms promise.then
1001ms unrelated setTimeout

Node runs its own event-loop callbacks inside InternalCallbackScope, whose Close() drains the nextTick queue and microtasks. ffi.getCurrentEventLoop() exists so native code can call back into JS from the loop, so those callbacks should get the same treatment.

What do you see instead?

0ms native timer callback
1001ms unrelated setTimeout
1002ms nextTick
1002ms microtask
1002ms promise.then

The queued work only runs after some unrelated Node callback. If none is pending, it runs during beforeExit. Any promise-based work started inside such a callback stalls until then.

Additional information

No response

Activity

  1. self-assigned this
    on Oct 22, 2026
  2. added
    ffiIssues and PRs related to experimental Foreign Function Interface support.
    on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

ffiIssues and PRs related to experimental Foreign Function Interface support.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions