Sometimes your application needs to let users customize a piece of behavior with JavaScript.
That code might be a function template for an AI agent, a customer-defined workflow, a plugin, a webhook transform, or an automation rule. You provide the shape of the function and the runtime context; users provide the custom logic. You want the useful part: programmable behavior without turning the rest of your application into the execution environment.
isolated-function gives you a small API for turning user-customizable function templates into controlled executions:
- give it a function or generated function template
- pass runtime arguments into it
- choose what it is allowed to do
- set memory and timeout limits
- run it
- get back the result, logs, errors, and timing data
It is useful when your product needs user-defined logic, but you still want a narrow execution contract around what that logic can receive, return, import, and access.
At a high level, isolated-function wraps a JavaScript function and runs it away from your main process.
The core use case is giving your product a controlled function template that users can customize. Your application owns the template, runtime arguments, dependencies, permissions, timeout, memory, result shape, logging, and cleanup. Users only customize the part of the function you intentionally expose.
This is the pattern used by @browserless/function: it lets users build functions that can access a Puppeteer page and related browser context, then runs those functions through isolated-function.
It can:
- run untrusted code in a separate process
- restrict filesystem, network, worker, and child-process access through the Node.js Permission Model
- stop runaway code with memory, wall-clock, and CPU limits
- detect dependencies used by the function
- install and bundle those dependencies with esbuild
- reuse installed dependencies across executions
- allow only the packages you explicitly trust
- return profiling data for compile, spawn, run, and total time
isolated-function is not a virtual machine, microVM, container runtime, or hypervisor-based sandbox.
It isolates JavaScript by running it in a separate Node.js process with Node.js permissions and resource limits. That is useful for many plugin, workflow, webhook, and AI-agent use cases, but it is not the same security boundary as KVM, Firecracker, Kata Containers, gVisor, or another VM-backed isolation layer.
If you need to execute hostile multi-tenant code, use isolated-function behind a stronger runtime boundary such as a locked-down container, gVisor, Kata Containers, Firecracker, or another hypervisor-backed execution environment.
pnpm add isolated-functionFirst create an isolated-function instance. You can call it with no options:
const isolatedFunction = require('isolated-function')()Or pass shared options that apply to every function created from that instance:
const isolatedFunction = require('isolated-function')({
tmpdir: '/tmp/isolated-function-deps',
nodePaths: [require('path').resolve(__dirname, 'node_modules')]
})- tmpdir controls where isolated-function installs and reuses dependencies.
- nodePaths points to directories where dependencies may already be installed, so matching packages can skip installation.
Then wrap a normal JavaScript function:
const sum = isolatedFunction((y, z) => y + z, {
memory: 128, // in MB
timeout: 10000 // in milliseconds
})Now call it like any other async function:
const { value, profiling } = await sum(3, 2)
console.log(value) // 5
console.log(profiling.total) // total execution time in millisecondsWhen your application shuts down, clean up the shared dependency cache:
await isolatedFunction.teardown()That is the basic loop: create a sandboxed function, run it, read the result.
By default, hosted code runs with minimal privileges. If it tries to use a restricted capability, isolated-function fails the execution instead of giving the code access.
For example, this function tries to write to /etc/passwd:
const fn = isolatedFunction(() => {
const fs = require('fs')
fs.writeFileSync('/etc/passwd', 'foo')
})
await fn()
// => PermissionError: Access to 'FileSystemWrite' has been restricted.The function ran in its own process, and the filesystem write was blocked.
Some functions need more access. Grant only the permissions that function needs with allow.permissions.
const fn = isolatedFunction(
() => {
const { execSync } = require('child_process')
return execSync('echo hello').toString().trim()
},
{
allow: { permissions: ['child-process'] }
}
)
const { value } = await fn()
console.log(value) // 'hello'See #allow.permissions for the full list.
Hosted code can bring its own dependencies. isolated-function parses require and import calls, installs the packages, and bundles the function before running it.
const isEmoji = isolatedFunction(input => {
/* this dependency only exists inside the isolated function */
const isEmoji = require('is-standard-emoji@1.0.0') // default is latest
return isEmoji(input)
})
await isEmoji('🙌') // => true
await isEmoji('foo') // => falseThe hosted code and its dependencies are bundled into a single file with esbuild before execution.
Dependencies are installed into a shared persistent directory and reused across invocations, so only the first call that requires a given package pays the install cost.
If the code is untrusted, do not let it install arbitrary packages. Use allow.dependencies to define the packages it may use:
const fn = isolatedFunction(
input => {
const isEmoji = require('is-standard-emoji')
return isEmoji(input)
},
{
allow: { dependencies: ['is-standard-emoji', 'lodash'] }
}
)
await fn('🙌') // => trueIf the code tries to require a package not in the allowed list, a DependencyUnallowedError is thrown before any package install happens:
const fn = isolatedFunction(
() => {
const malicious = require('malicious-package')
return malicious()
},
{
allow: { dependencies: ['lodash'] }
}
)
await fn()
// => DependencyUnallowedError: Dependency 'malicious-package' is not in the allowed listSecurity Note: Even with the sandbox, arbitrary package installation is dangerous because packages can execute code during installation via
preinstall/postinstallscripts. The--ignore-scriptsflag is used to mitigate this, but providing anallow.dependencieswhitelist is the recommended approach for running untrusted code.
Any hosted code execution will be run in their own separate process:
/** make a function to consume ~128MB */
const fn = isolatedFunction(() => {
const storage = []
const oneMegabyte = 1024 * 1024
while (storage.length < 78) {
const array = new Uint8Array(oneMegabyte)
for (let ii = 0; ii < oneMegabyte; ii += 4096) {
array[ii] = 1
}
storage.push(array)
}
})
const { value, profiling } = await fn()
console.log(profiling)
// {
// cpu: 42.5,
// memory: 128204800,
// phases: {
// compile: 0,
// spawn: 48,
// run: 54,
// total: 102
// }
// }Each execution includes profiling data:
- cpu — CPU time (user + system) consumed by the process, in milliseconds.
- memory — Peak RSS (Resident Set Size) of the process, in bytes.
- phases — Wall-clock time breakdown of each execution stage, in milliseconds:
- compile — Time waiting for code compilation (dependency detection, package install, esbuild bundling). This is
0after the first call since the result is cached. - spawn — Process creation, Node.js boot, and template setup overhead.
- run — User function execution time.
- total — End-to-end wall-clock time.
- compile — Time waiting for code compilation (dependency detection, package install, esbuild bundling). This is
You can limit a isolated-function with memory:
const fn = isolatedFunction(() => {
const storage = []
const oneMegabyte = 1024 * 1024
while (storage.length < 78) {
const array = new Uint8Array(oneMegabyte)
for (let ii = 0; ii < oneMegabyte; ii += 4096) {
array[ii] = 1
}
storage.push(array)
}
}, { memory: 64 })
await fn()
// => MemoryError: Out of memoryor by execution time:
const fn = isolatedFunction(() => {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
await delay(duration)
return 'done'
}, { timeout: 50 })
await fn(100)
// => TimeoutError: Execution timed outThe timeout option enforces both a wall-clock limit (SIGKILL) and a CPU time limit (RLIMIT_CPU). A CPU-bound infinite loop will be terminated by the kernel via SIGXCPU:
const fn = isolatedFunction(() => {
while (true) { Math.random() }
}, { timeout: 5000 })
await fn()
// => CpuTimeError: CPU time limit exceededThe logs are collected into a logging object returned after the execution:
const fn = isolatedFunction(() => {
console.log('console.log')
console.info('console.info')
console.debug('console.debug')
console.warn('console.warn')
console.error('console.error')
return 'done'
})
const { logging } = await fn()
console.log(logging)
// {
// log: ['console.log'],
// info: ['console.info'],
// debug: ['console.debug'],
// warn: ['console.warn'],
// error: ['console.error']
// }Any error during isolated-function execution will be propagated:
const fn = isolatedFunction(() => {
throw new TypeError('oh no!')
})
const result = await fn()
// TypeError: oh no!You can also return the error instead of throwing it with { throwError: false }:
const fn = isolatedFunction(
() => {
throw new TypeError('oh no!')
},
{ throwError: false }
)
const { isFulfilled, value } = await fn()
if (!isFulfilled) {
console.error(value)
// TypeError: oh no!
}Creates an isolated-function instance. All functions created from the same instance share the same dependencies directory.
Type: string
Default: path.join(os.tmpdir(), 'isolated-fn-deps')
The directory used for installing code dependencies. Dependencies are installed once and reused across invocations, so only the first call that requires a given package pays the install cost.
const isolatedFunction = require('isolated-function')({
tmpdir: '/tmp/my-isolated-deps'
})Required
Type: function
The hosted function to run.
Type: number
Default: Infinity
Set the function memory limit, in megabytes.
Type: boolean
Default: true
When false, returns the error instead of throwing it as { value: error, isFulfilled: false }.
Type: number
Default: Infinity
Timeout after a specified amount of time, in milliseconds. Enforces both a wall-clock limit (via SIGKILL) and a CPU time limit (via RLIMIT_CPU/SIGXCPU).
Type: object
Default: {}
Configuration object for allowed permissions and dependencies.
const fn = isolatedFunction(
() => {
const { execSync } = require('child_process')
const lodash = require('lodash')
return lodash.uniq([1, 2, 2, 3])
},
{
allow: {
permissions: ['child-process'],
dependencies: ['lodash']
}
}
)Type: string[]
Default: []
An array of permissions to grant to the isolated function based on Node.js Options
When empty, the function runs with minimal privileges and will throw an error if it attempts to access restricted resources. Available permissions are:
-
addons— e.g.require('native-module')
Allow loading native C++ addons. -
child-process— e.g.execSync('echo hello')
Allow spawning child processes viachild_processmodule. -
ffi(Node.js v26+) — e.g.ffi.open('libc.so')
Allow foreign function interface calls to shared libraries. -
fs-read— e.g.fs.readFileSync('/etc/hosts')
Allow reading from the filesystem. Supports path scoping:fs-read=/etc/hosts. -
fs-write— e.g.fs.writeFileSync('/tmp/out.txt', data)
Allow writing to the filesystem. Supports path scoping:fs-write=/tmp. -
inspector— e.g.require('inspector').open()
Allow the inspector protocol for debugging. -
net(Node.js v25+) — e.g.http.get('http://example.com')
Allow outbound network connections. -
wasi— e.g.new WASI({ args, env })
Allow WebAssembly System Interface operations. -
worker— e.g.new Worker('./task.js')
Allow creating worker threads.
Type: string[]
Default: undefined
A whitelist of package names that are allowed to be installed. When provided, only packages in this list can be required/imported by the isolated function.
This is a critical security feature when running untrusted code, as it prevents arbitrary package installation which could lead to remote code execution via malicious packages.
const fn = isolatedFunction(
() => {
const lodash = require('lodash')
const axios = require('axios')
return lodash.get({ a: 1 }, 'a')
},
{
allow: { dependencies: ['lodash', 'axios'] }
}
)When allow.dependencies is not provided, any package can be installed (default behavior for backwards compatibility).
Type: function
The isolated function to execute. You can pass arguments over it.
Type: function
Removes the shared dependencies directory. Call this once when shutting down the server to clean up resources.
await isolatedFunction.teardown()Default: true
When is false, it disabled minify the compiled code.
Pass DEBUG=isolated-function for enabling debug timing output.
isolated-function © Kiko Beats, released under the MIT License.
Authored and maintained by Kiko Beats with help from contributors.
kikobeats.com · GitHub @Kiko Beats · X @Kikobeats
