NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...5 min read

Show HN: QuickBEAM – run JavaScript as supervised Erlang/OTP processes

Share
NOW LET US Article – Show HN: QuickBEAM – run JavaScript as supervised Erlang/OTP processes

QuickBEAM is a new JavaScript runtime for the BEAM virtual machine, allowing JS to run as supervised GenServer processes with native access to Erlang/OTP libraries and messaging.

JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM, and a built-in TypeScript toolchain.

JS runtimes are GenServers. They live in supervision trees, send and receive messages, and call into Erlang/OTP libraries — all without leaving the BEAM.

def deps do
[{:quickbeam, "~> 0.7.1"}]
end

Requires Zig 0.15+ (installed automatically by Zigler, or use system Zig).

{:ok, rt} = QuickBEAM.start()
{:ok, 3} = QuickBEAM.eval(rt, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.eval(rt, "'hello'.toUpperCase()")
# State persists across calls
QuickBEAM.eval(rt, "function greet(name) { return 'hi ' + name }")
{:ok, "hi world"} = QuickBEAM.call(rt, "greet", ["world"])
QuickBEAM.stop(rt)

JS can call Elixir functions and access OTP libraries:

{:ok, rt} = QuickBEAM.start(handlers: %{
"db.query" => fn [sql] -> MyRepo.query!(sql).rows end,
"cache.get" => fn [key] -> Cachex.get!(:app, key) end,
})
{:ok, rows} = QuickBEAM.eval(rt, """
const rows = await Beam.call("db.query", "SELECT * FROM users LIMIT 5");
rows.map(r => r.name);
""")

JS can also send messages to any BEAM process:

// Get the runtime's own PID
const self = Beam.self();
// Send to any PID
Beam.send(somePid, {type: "update", data: result});
// Receive BEAM messages
Beam.onMessage((msg) => {
console.log("got:", msg);
});
// Monitor BEAM processes
const ref = Beam.monitor(pid, (reason) => {
console.log("process died:", reason);
});
Beam.demonitor(ref);

| Category | API | Description | |---|---|---| | Bridge | Beam.call(name, ...args) | Call an Elixir handler (async) | | | Beam.callSync(name, ...args) | Call an Elixir handler (sync) | | | Beam.send(pid, message) | Send a message to a BEAM process | | | Beam.onMessage(callback) | Receive BEAM messages | | Process | Beam.self() | PID of the owning GenServer | | | Beam.spawn(script) | Spawn a new JS runtime as a BEAM process | | | Beam.register(name) | Register the runtime under a name | | | Beam.whereis(name) | Look up a registered runtime | | | Beam.monitor(pid, callback) | Monitor a process for exit | | | Beam.demonitor(ref) | Cancel a monitor | | | Beam.link(pid) / Beam.unlink(pid) | Bidirectional crash propagation | | Distribution | Beam.nodes() | List connected BEAM nodes | | | Beam.rpc(node, runtime, fn, ...args) | Remote call to another node | | Utilities | Beam.sleep(ms) / Beam.sleepSync(ms) | Async/sync sleep | | | Beam.hash(data, range?) | Non-cryptographic hash (:erlang.phash2 ) | | | Beam.escapeHTML(str) | Escape & < > " ' | | | Beam.which(bin) | Find executable on PATH | | | Beam.peek(promise) / Beam.peek.status(promise) | Read promise result without await | | | Beam.randomUUIDv7() | Monotonic sortable UUID | | | Beam.deepEquals(a, b) | Deep structural equality | | | Beam.nanoseconds() | Monotonic high-res timer | | | Beam.uniqueInteger() | Monotonically increasing unique integer | | | Beam.makeRef() | Create a unique BEAM reference | | | Beam.inspect(value) | Pretty-print any value (including PIDs/refs) | | Semver | Beam.semver.satisfies(version, range) | Check version against Elixir requirement | | | Beam.semver.order(a, b) | Compare two semver strings | | Password | Beam.password.hash(password, opts?) | PBKDF2-SHA256 hash | | | Beam.password.verify(password, hash) | Constant-time verification | | Introspection | Beam.version | QuickBEAM version string | | | Beam.systemInfo() | Schedulers, memory, atoms, OTP release | | | Beam.processInfo() | Memory, reductions, message queue |

Runtimes and context pools are OTP children with crash recovery:

children = [
{QuickBEAM,
name: :renderer,
id: :renderer,
script: "priv/js/app.js",
handlers: %{
"db.query" => fn [sql, params] -> Repo.query!(sql, params).rows end,
}},
{QuickBEAM, name: :worker, id: :worker},
# Context pool for high-concurrency use cases
{QuickBEAM.ContextPool, name: MyApp.JSPool, size: 4},
]
Supervisor.start_link(children, strategy: :one_for_one)
{:ok, html} = QuickBEAM.call(:renderer, "render", [%{page: "home"}])

The :script option loads a JS file at startup. If the runtime crashes, the supervisor restarts it with a fresh context and re-evaluates the script.

Individual Context processes are typically started dynamically (e.g. from a LiveView mount) and linked to the connection process.

For high-concurrency scenarios (thousands of connections), use ContextPool instead of individual runtimes. Many lightweight JS contexts share a small number of runtime threads:

# Start a pool with N runtime threads (defaults to scheduler count)
{:ok, pool} = QuickBEAM.ContextPool.start_link(name: MyApp.JSPool, size: 4)
# Each context is a GenServer with its own JS global scope
{:ok, ctx} = QuickBEAM.Context.start_link(pool: MyApp.JSPool)
{:ok, 3} = QuickBEAM.Context.eval(ctx, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.Context.eval(ctx, "'hello'.toUpperCase()")
QuickBEAM.Context.stop(ctx)

Contexts support the full API — eval, call, Beam.call/callSync, DOM, messaging, browser/node APIs, handlers, and supervision:

# In a Phoenix LiveView
def mount(_params, _session, socket) do
{:ok, ctx} = QuickBEAM.Context.start_link(
pool: MyApp.JSPool,
handlers: %{"db.query" => &MyApp.query/1}
)
{:ok, assign(socket, js: ctx)}
end

The context is linked to the LiveView process — it terminates and cleans up automatically when the connection closes. No explicit terminate callback needed.

Contexts can load individual API groups instead of the full browser bundle:

QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :fetch]) # 231 KB
QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :url]) # 108 KB
QuickBEAM.Context.start_link(pool: pool, apis: false) # 58 KB
QuickBEAM.Context.start_link(pool: pool) # 429 KB (all browser APIs)

Available groups: :fetch, :websocket, :worker, :channel, :eventsource, :url, :crypto, :compression, :buffer, :dom, :console, :storage, :locks. Dependencies auto-resolve.

{:ok, ctx} = QuickBEAM.Context.start_link(
pool: pool,
memory_limit: 512_000, # per-context allocation limit (bytes)
max_reductions: 100_000 # opcode budget per eval/call
)
# Track per-context memory
{:ok, %{context_malloc_size: 92_000}} = QuickBEAM.Context.memory_usage(ctx)

Exceeding memory_limit triggers OOM. Exceeding max_reductions interrupts the current eval but keeps the context usable for subsequent calls.

QuickBEAM can load browser APIs, Node.js APIs, or both:

# Browser APIs only (default)
QuickBEAM.start(apis: [:browser])
# Node.js compatibility
QuickBEAM.start(apis: [:node])
# Both
QuickBEAM.start(apis: [:browser, :node])
# Bare QuickJS engine — no polyfills
QuickBEAM.start(apis: false)

Like Bun, QuickBEAM implements core Node.js APIs. BEAM-specific extensions live in the Beam namespace.

{:ok, rt} = QuickBEAM.start(apis: [:node])
QuickBEAM.eval(rt, """
const data = fs.readFileSync('/etc/hosts', 'utf8');
const lines = data.split('\\n').length;
lines
""")
# => {:ok, 12}

| Module | Coverage | |---|---| | process | env, cwd(), platform, arch, pid, argv, version, nextTick, hrtime, stdout, stderr | | path | join, resolve, basename, dirname, extname, parse, format, relative, normalize, isAbsolute, sep, delimiter | | fs | readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, lstatSync, unlinkSync, renameSync, rmSync, copyFileSync, realpathSync, readFile, writeFile | | os | platform(), arch(), type(), hostname(), homedir(), tmpdir(), cpus(), totalmem(), freemem(), uptime(), EOL, endianness() |

process.env is a live Proxy — reads and writes go to System.get_env / System.put_env.

{:ok, rt} = QuickBEAM.start(
memory_limit: 10 * 1024 * 1024, # 10 MB heap
max_stack_size: 512 * 1024 # 512 KB call stack
)
© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.