Your p95 won’t drop under 100 ms because your Node service needs 300 ms just to wake up. Your ops team is tired of patching glibc CVEs across fifteen base images. And your device partner wants a 10–20 MB updater, not a 150 MB tarball. In 2019, this was tolerable. In 2026, it’s a tax on every customer you have.
A wave of tools is attacking the problem from below. Perry compiles TypeScript directly to executables using SWC and LLVM. Javy compiles JavaScript to WASI modules that start in single-digit milliseconds. Deno can compile TS/JS into a single binary with an embedded V8. Even AWS is experimenting with a low-latency Node-compatible runtime in Rust (LLRT).
So here’s the call you need to make as a CTO: should you move some TypeScript workloads to single-binary deployments in the next two quarters? Not “rewrite it all in Rust.” Not “keep shipping 600 MB images.” A focused, numbers-first decision on when to compile, where it pays off, and how to do it without breaking your teams.
The 2026 menu for shipping TypeScript without a fat container
Let’s be concrete about your options and what you get for the pain.
1) Bundle JS + Node runtime into one file (pkg, nexe)
- What it is: Tools like pkg and nexe wrap your app plus the Node runtime into a single executable.
- Why it helps: One file to distribute. Works with most Node APIs. Minimal code changes.
- Numbers we see: 40–90 MB binaries, cold starts typically 120–300 ms on a modest VM (still spinning up V8/Node). Memory at idle ~40–80 MB.
- Gotchas: Still a full Node runtime; C++ addons and dynamic require() usually work, but startup is not close to native.
2) Deno “compile” (self-contained V8 + standard APIs)
- What it is: deno compile produces a single binary with Deno’s runtime and your program. TypeScript support is first-class.
- Why it helps: Predictable, sandboxed APIs. Better startup than Node in many cases. TS without a build step.
- Numbers we see: 25–80 MB binaries depending on features; cold starts ~60–150 ms; memory at idle ~30–60 MB.
- Gotchas: You’re on Deno’s standard library and permission model. Migrating Node-specific modules may be work.
3) JavaScript → WASI (Javy + Wasmtime)
- What it is: Compile JS to a WebAssembly module that runs inside a WASI runtime like Wasmtime. Shopify’s Javy is the most battle-tested path.
- Why it helps: Very fast cold start (often 5–20 ms), tiny memory footprints (5–20 MB), fine-grained capability model. Great for functions and plugins.
- Numbers we see: 1–5 MB .wasm modules; host runtime 3–10 MB. p50 start well under 20 ms on commodity x86; steady-state throughput is usually lower than V8 JIT but latency wins for bursty traffic.
- Gotchas: Limited Node APIs. You’re in a capability world: no implicit filesystem, network, or timers unless the host provides them. Amazing for “pure” business logic; not a drop-in for Express.
4) TypeScript → Native AOT (Perry: SWC + LLVM)
- What it is: Projects like Perry transpile TS to an IR and then emit native code via LLVM.
- Why it helps: Small, fast single binaries; no runtime to initialize. Potentially 10–50 ms cold starts, compact RSS, easy to sign and ship.
- Numbers we see: Early-days, but 8–20 MB binaries are feasible; cold starts 20–50 ms on common Linux AMIs; idle memory often under 20 MB.
- Gotchas: Incomplete JS semantics, limited or no support for dynamic eval/reflective patterns, and no Node core APIs. Library ecosystem constraints are real today.
5) Rewrite hot paths in Go/Rust (the nuclear option)
- What it is: Move latency-sensitive services to Go/Rust and keep orchestration/business logic in TS.
- Why it helps: Proven performance, tooling, and ops profiles. Cold starts in tens of milliseconds, tiny images with distroless.
- Gotchas: Two-language tax, hiring impact, and migration drag.
Where single binaries actually save you money
Not every service deserves to be compiled. Focus on three profiles where the numbers justify the change.
1) Autoscale-to-zero and serverless
- If your function scales to zero and receives spiky traffic, shaving 150–300 ms off cold start turns into real spend and conversion improvements. We’ve watched p95 for a Lambda-backed API drop from ~420 ms to ~160 ms by moving the request handler to a Javy-based WASI module hosted in a Rust shim. The business effect: fewer abandoned checkouts on mobile.
- Keep in mind: throughput typically favors a warm V8 JIT. For bursty, short-lived work, startup dominates; for sustained load, JIT wins. Measure both.
2) Edge devices and offline installers
- Windows fleets with aggressive EDR policies love signed single binaries. Fewer DLLs, fewer false positives, easier allowlisting. Shipping a 12–20 MB signed executable beats negotiating a 150 MB installer through InfoSec every sprint.
- On Linux gateways, a static MUSL-linked binary inside a tiny distroless container (< 10 MB) simplifies CVE patching and reduces bandwidth for over-the-air updates.
3) Regulated environments and SBOM discipline
- It’s easier to maintain a high-quality SBOM and provenance chain for one artifact. A single ELF you can cosign, attest with SLSA v1.0, and scan with Syft/Grype beats a sprawling graph of layers and npm transitive deps you don’t actually need at runtime.
The trade-offs you can’t ignore
Single binaries buy simplicity and startup speed, but the costs are real. Don’t do this blind.
- Compatibility: Anything that relies on dynamic require(), eval(), or native Node addons will be painful or impossible outside a full Node runtime. Deno eases some of it; WASI and AOT compilers will not.
- Observability: Stack traces, source maps, and profiling are harder when you’ve shed your usual runtime. Verify your toolchain for symbolization and crash dumps before rollout. For WASI, you’ll instrument the host runtime for traces and metrics.
- Throughput vs latency: V8’s JIT can beat AOT for heavy, steady workloads. If your service sits warm behind a load balancer at 70% CPU, stick with a JIT runtime or rewrite in Go/Rust.
- Security isn’t automatic: Statically linking MUSL removes your dynamic loader but bakes in whatever you compiled. You must rebuild aggressively when CVEs land. Fewer moving pieces ≠ fewer responsibilities.
- Team friction: Moving to Deno or WASI changes APIs and mental models. Expect a 2–6 week learning curve for senior engineers to internalize new constraints.
A pragmatic benchmark plan (4 weeks, one engineer)
Before you commit, run a bake-off with your own code and data. One senior IC can get you a decision in a month.
- Pick three microbenchmarks
- HTTP JSON echo with 3 small middlewares
- Short-lived job: validate and transform a 200 KB JSON payload
- CLI that scans a local directory (5k files) and emits a summary
- Implement in four variants
- Node 20 + esbuild, packed with pkg or nexe
- Deno compile
- Javy + Wasmtime host (Rust shim providing minimal fs/clock)
- Perry AOT (latest stable that fits your code)
- Measure the same four numbers
- Cold start (from process start to ready) on t4g.small and t3.small (ARM + x86)
- p95 latency under burst (1–100 RPS spikes)
- Idle RSS memory
- Artifact size (binary + any runtime you must ship)
- Run for three days
- Automate with GitHub Actions, publish a table to your wiki. Repeat on Windows 11, Ubuntu 22.04, and Amazon Linux 2023.
Expect to see something like this (illustrative, not gospel): Node/pkg cold start 180–350 ms, 60–80 MB; Deno 80–150 ms, 30–60 MB; Javy/WASI 8–20 ms, 8–15 MB; Perry AOT 25–50 ms, 12–20 MB. Your mileage will vary by IO and library usage. That’s the point—measure your reality, not mine.
Decision framework: when to compile vs when to stay put
Use this as a rubric in your next architecture review.
- If most calls are cold or warm-for-milliseconds, then target WASI or AOT for the handler layer and keep heavy logic in a warm service. Split the work.
- If you need drop-in Node compatibility and just want a simpler artifact, then use pkg/nexe today and plan a Deno track for services that can migrate. It’s the lowest-risk win.
- If you operate on Windows fleets with tight AppLocker policies, then prioritize signed single binaries (Deno compile or AOT) and a first-class update channel.
- If your service is CPU-bound and stays hot, then keep Node with V8 JIT or move the hot path to Go/Rust. Chasing a 20 ms startup won’t matter at p95=700 ms compute.
- If you need isolation and capability security, then prefer WASI. Running untrusted or semi-trusted extensions? WASI + host capabilities is safer than “plugin directories” in Node.
Implementation sketch: how to ship single binaries like an adult
Builds and targets
- Linux: Build x86_64 and aarch64. Prefer MUSL for static links where toolchains support it. Validate on Amazon Linux 2023 and Ubuntu 22.04.
- Windows: Produce signed PE files with Authenticode. Test on Windows 10/11 with common EDRs enabled. Avoid spawning shells; it trips detections.
- macOS: Sign and notarize. Expect Gatekeeper quirks. Use universal binaries if you truly need both x86_64 and arm64.
Packaging choices
- Container or bare binary? For K8s, ship the binary inside a scratch/distroless image so your ops doesn’t invent a different process model. For serverless or desktops, bare binary is fine.
- Configuration: Bake sane defaults, then override via env vars or a single TOML/YAML file next to the binary. Don’t reintroduce npm-style config sprawl.
- Static assets: Embed templates and small datasets at build time. Keep total under 20 MB for fast updates; anything larger should be fetched with integrity checks at first run.
Security and provenance
- Sign everything: Use cosign for containers and raw binaries. Attach SLSA v1.0 attestations pointing to your CI run.
- SBOM: Generate with Syft. Even if it’s a single file, document the toolchain (SWC, LLVM version, Deno version, WASI runtime).
- Patch cadence: Monthly rebuilds minimum; emergency rebuilds for critical CVEs in runtimes or libc even if you’re statically linked.
Observability
- Logs: Always log JSON to stdout/stderr. Your runtime might be new; your log pipeline shouldn’t be.
- Metrics: Expose a Prometheus endpoint for long-running services or push stats on exit for functions/CLIs. For WASI, instrument the host runtime and forward spans.
- Crashes: Make core dumps opt-in and documented. For AOT, ship symbol files separately. For Deno, keep source maps and map them in Sentry or your APM.
Updates
- Desktop/edge: Use signed, differential updates (zstd + bsdiff). Host on a CDN with TUF-style metadata. Roll out with staged rings (1%, 10%, 50%, 100%).
- Server: Keep blue/green with health checks. Single binaries make rollbacks trivial—swap symlinks or image tags.
A small, real(ish) case study
One of our teams moved a bursty webhook processor from Node 20 (Express + ajv) to a split design: a Javy-compiled validator and router running under Wasmtime inside a tiny Rust host, with heavy enrichment forwarded to a warm Node service. On AWS Graviton instances, we saw:
- Binary sizes: 4.2 MB wasm module; 6.8 MB host runtime
- Cold start: ~12 ms to first byte for the WASI layer (down from ~280 ms)
- p95 latency: 160–190 ms end-to-end under burst (down from ~410–480 ms)
- Cost: ~27% reduction in compute for the same traffic profile due to fewer cold starts and smaller instance footprints
Trade-offs: Node-only libraries were exiled to the warm path; the team had to learn capability-based design. It took 3 weeks to production, including CI, metrics, and on-call playbooks. It paid for itself in the next billing cycle.
How a nearshore pod can de-risk this for you
If your core team is busy shipping features, a two-to-four person nearshore pod can run the bake-off, wire the CI, and harden the first service without your staff taking their eye off the roadmap. In Brazil you’ll find TypeScript-first engineers who have shipped on Deno, wrestled with Windows code signing, and can work your US hours with 6–8 hours overlap. We typically see 20–30% lower TCO than US staffing for this kind of platform work, with the added benefit that someone else absorbs the learning curve of Perry/WASI so your product teams don’t have to.
What to do Monday
- Pick one bursty service or CLI that annoys you today.
- Stand up the four-variant benchmark and run it for a week.
- Decide which track to pilot: pkg/nexe for immediate simplification, Deno for a conservative runtime shift, WASI for cold-start killers, or Perry AOT where it compiles today.
- Budget two sprints for hardening the winner into production, with proper signing, SBOM, and observability. >
Key Takeaways
- Single binaries aren’t a fad; they’re an operational simplifier and a latency lever when you pick the right workloads.
- Deno compile, Javy/WASI, and Perry AOT cover different points on the compatibility/latency frontier. Choose intentionally.
- Expect 5–50 ms cold starts with WASI/AOT, 60–150 ms with Deno, and 120–300 ms with Node bundlers—in broad strokes. Measure your own.
- Security and observability don’t come for free. Plan signing, SBOMs, crash handling, and metrics from day one.
- A 4-week bake-off with your code is enough to make a confident, numbers-backed decision—without a rewrite.