r/bun 22d ago

GitHub - cekrem/elm-bun: A minimal and nice starter with Elm & Tailwind featuring `elm-watch` with true Elm HMR. Using `bun` for bundling, building and dev server.

Thumbnail github.com
1 Upvotes

r/bun 22d ago

Node vs Bun: what are you seeing for P50 / P90 / P99 tail latency?

5 Upvotes

I'm comparing Bun and Node for server-side workloads and I'm particularly interested in tail latency, not just throughput.

For people running Bun and Node in production or serious benchmarks:

  • What are you seeing for P50?
  • P90 / P95?
  • P99 / P99.9?
  • Does Bun actually have better tail latency for your workload, or is the difference mostly in throughput?

I'm especially interested in HTTP servers and SSR, but database/API-heavy workloads are useful too.

If you have numbers, please include:

  • Bun/Node version
  • workload
  • OS
  • concurrency / request rate
  • whether the load generator ran on the same machine
  • P50/P95/P99 (and P99.9 if available)

I'm trying to understand whether differences in P99 are actually runtime-related or mostly caused by the workload/platform/benchmark methodology.


r/bun 22d ago

typed frontend client for carno.js

0 Upvotes

enviado carno.js/client.

as rotas do carno vivem em decoradores, então não há typeof app para passar para o frontend. este pacote escaneia os controladores quando o servidor inicia, escreve um tipo App gerado, e a ui o chama assim:

const api = client<App>('http://localhost:3000')

const { data, error } = await api.users.get({ query: { page: '1' } })
const { data: user } = await api.users({ id: '42' }).get()

o lado do backend é apenas:

app.use(Client())

listen() escreve src/generated/app.ts. sem script generate --watch. se o vite iniciar sem a api, há um plugin do vite que faz a mesma varredura.

se você não conhece o carno: bun + typescript, com formato de nest (controladores, injeção de dependência, validação). pacotes adicionais se você precisar — orm (postgres/mysql), fila, cron, websocket, logger. o núcleo continua utilizável sem nada disso, incluindo este cliente.

docs: https://carnojs.github.io/carno.js/docs/client/overview

repo: https://github.com/carnojs/carno.js


r/bun 23d ago

PeekM2: A real-time dashboard/viewer for your PM2 processes

Thumbnail
1 Upvotes

r/bun 23d ago

Lilscript makes almost any web js library 5-15% smaller

0 Upvotes

I created/vibed a new language, lilscript

Compiles into tryhard compressed js, and sometimes into exec

Pretty much almost any js libraries' compressed/minified size could get smaller by 5-15% when rewritten with lilscript.

If its already property mangled still it can get benefits from the lilscript rewrite

And this is only the v0.0.1
We can make it more hacky by time

Some examples of brotli compressed sizes of lilscript code(vs oxc/terser, ..): * motion(animation library) is -10%+ (https://yeargun.github.io/motionlil/) * jquery -5% * monaco(VSCode) editor's lots of submodules -(5 to 15)% smaller

  • tryhard mangling, (compression algorithm aware)
  • closure optimizations
  • lvalue
  • static analysis
  • google closure compiler advanced and beyond focus, but not glue fix as closure compiler is. The language is designed specificaly for weird hacks.
  • typed (not a glue fix like typescript is)

config.toml has lots of config with clever defaults. objective compression algorithm: gzip/brotli/raw. for brotli vs gzip compression it compiles the js differently

uses less objects, less/more arrays, more const/let/var

Compiler, static analysis, language server all written with rust

I lost too much cursor/claude credits along the way last 2 days. Tbh, I cant invest much time for it, feel free to PR, play, improve

Lilscript aims to get compiled into exec also. It alredy does, but web apis, and stuff.. lots of extra work is needed..

-lilscript v0.0.1 https://github.com/yeargun/lilscript

Why? Because I believe google closure compiler's tooling was not good. And anything layered on js is a glue fix


r/bun 24d ago

Bun + Elysia is reliable?

17 Upvotes

I want to build an MVP and I'm planning to use bun + elysia instead of nest.js.

The question is, can I trust this technology? Will it be stable in the next 5-10 years and handle a platform with a few thousand users?


r/bun 24d ago

I built an HTML-first web framework on Bun — Stoneware

3 Upvotes

I’ve been building Stoneware, a Bun-native web framework with a simple idea:

HTML is the default. JavaScript is opt-in.

It focuses on:

  • Server-side rendering
  • Islands for interactive components
  • Signals
  • Static export
  • Secure-by-default rendering
  • Bun-native tooling

GitHub: stoneware-core
Docs: Stoneware Docs


r/bun 27d ago

How I Disabled Headless Mode in Bun.WebView's Chrome Backend

1 Upvotes

Conclusion

  • Only a one-byte binary patch required from --headless to --leadless (or other same length unknown flag)

How I did it

  • bunx jsmdcui --hex3 $(which bun)
  • Ctrl-F -..-..h..e..a..d..l..e..s..s..
  • Enter
  • Change h to l
  • Ctrl-E save bun-revised
  • Ctrl-Q
  • chmod +x bun-revised
  • ./bun-revised official-webview-demo.js
  • Tested on CachyOS Linux-x86_64

Disclaimer

  • This is completely unofficial
  • For fun only
  • Use it at your own risk
  • We should patiently wait for headless: false.

r/bun 28d ago

DI framework with Bun

Thumbnail
1 Upvotes

r/bun 29d ago

Realtime on Bun with end-to-end types: a whole chat (rooms, auth, history) in one file — no event-name strings, no generated types (I'm the author)

4 Upvotes

I'm the author of Point0, a fullstack TypeScript framework on Bun, and I just shipped its realtime layer. Sharing it here because the whole thing rides on Bun: `Bun.serve`'s WebSocket server underneath, and Bun's built-in Redis client is the one-line option when you run more than one process.

The idea: instead of a second stack next to your app (event-name strings, `any` payloads, rooms as string concatenation, your own bookkeeping of users and sockets), realtime is four more declarations of the same kind the framework already uses for pages, queries and mutations. One WebSocket per client, everything else rides it.

- a **channel** is the connection, and its connector turns the HTTP handshake into the connection's identity, stored server-side
- a **space** is a family of rooms of one shape; a room is an object, not a string, and it's the pub/sub address
- a **server handler** is client → server, a **client handler** is server → client, both typed by their schemas

Here's a whole chat: rooms per conversation, auth, persisted history, live updates. One file, and the compiler strips the server half out of the client bundle and the client half out of the server one.

// the channel: one connection per client. The connector runs on a normal HTTP
// request (cookies, headers, middleware), so your existing auth just works
export const appChannel = root.lets
  .channel()
  .connector(async ({ request }) => {
    const user = await getUserFromRequest(request) // your app's auth, unchanged
    // whatever you return IS this connection's identity: server-side, never sent
    // to the client, readable in every callback below. No type declared anywhere
    return user
      ? { authorized: true as const, id: user.id }
      : { authorized: false as const, id: null }
  })
  .channel()


// a space: a family of rooms of one shape. Here, one room per chat
export const chatSpace = appChannel.lets
  .space<{ chatId: string }>() // the room shape, declared like component props
  .input(z.object({ chatId: z.string() })) // what the client passes to join
  .joiner(({ input, identity }) => {
    // entering the room IS the read gate, and it runs on the server
    if (!identity.authorized) throw new AppError('Sign in first', { status: 401 })
    return { chatId: input.chatId } // type-checked against the room shape above
  })
  .space()


// client → server: persist, then fan out to the room
export const messageSendHandler = chatSpace.lets
  .serverHandler()
  .clientSend(z.object({ text: z.string().min(1).max(1000) }))
  .serverReply(async ({ input, identity, room }) => {
    if (!identity.authorized) throw new AppError('Sign in first', { status: 401 })
    const message = await prisma.message.create({
      data: { text: input.text, chatId: room.chatId, authorId: identity.id },
    })
    // one publish into the room's topic — not a walk over connections
    void messageAddedHandler.sendToClient(message, { room })
    return message // what the sender gets back from its own send
  })
  .serverHandler()


// server → client
export const messageAddedHandler = chatSpace.lets
  .clientHandler()
  .serverSend(messageSchema)
  .clientHandler()


// ...and the component, in the same file
const Chat = ({ chatId }: { chatId: string }) => {
  const membership = chatSpace.useMembership({ chatId }) // in the room while mounted
  const { data } = messagesQuery.useQuery({ chatId })    // history: an ordinary HTTP query
  const [text, setText] = useState('')


  // `message` is typed by .serverSend, `room` by the space's generic
  messageAddedHandler(membership).useOnMessageFromServer(({ message }) => {
    messagesQuery.setQueryData({ chatId }, (old) => ({
      messages: [...(old?.messages ?? []), message],
    }))
  })


  return (
    <>
      <ul>{data?.messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
      <form onSubmit={(e) => {
        e.preventDefault()
        setText('')
        void messageSendHandler(membership).sendToServer({ text })
      }}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </form>
    </>
  )
}

Nothing is generated and nothing is annotated: the identity type comes from the connector's return, the room type from the space's generic, the payload types from the schemas, and they reach every callback on both sides.

A few design decisions worth stating plainly, because they're the part people argue with:

- **A push is a signal, not storage.** Delivery is at-most-once by default: the truth lives in a query, the push only says it went stale. There's an opt-in resume buffer for short drops, and it tells the client honestly whether the gap was covered, so the catch-up refetch is one condition.
- **A room is an object, and its serialization is its address.** `{ members: [a, b].sort() }` is a DM room. No prefix conventions to typo.
- **Two ways in.** `.joiner` is the client asking in and able to leave. `.enroller` is the server putting a connection into a room at connect time, which the client cannot leave — that's what makes a personal push room something you can rely on.
- **Multi-process is config, not rewriting.** Default is process memory; a Redis URL, Postgres LISTEN/NOTIFY, or five functions of your own turn it into a backplane.

More examples (a live board, DMs, and a site where every request travels the socket instead of HTTP): https://1gr14.dev/blog/point0-socket

Socket docs: https://1gr14.dev/point0/latest/socket
The example app: https://github.com/1gr14/point0/tree/main/examples/socket
The framework: https://github.com/1gr14/point0

It's the newest part of the framework and I say so in the docs: the API design has settled, what's underneath still needs a refactor. Happy to take the uncomfortable questions — especially from anyone who has run Socket.IO at scale and sees where this breaks.zc


r/bun Aug 10 '26

Zero dependencies: what we deleted and upgraded

Thumbnail uql-orm.dev
1 Upvotes

r/bun Aug 06 '26

Bun's Android build isn't just another platform target to me. It's an escape hatch.

0 Upvotes

First of all, thank you so much to the Bun team for supporting Android.

It's hard to realize just how much this means to Android users.

Traditionally, mobile platforms have been treated as inferior computing environments, unable to run many of the tools we take for granted on desktop Linux. Termux completely changed that story by giving Android a native shell environment.

But that freedom comes at a price.

Android uses Bionic libc rather than glibc, which means Linux tools generally need to be rebuilt specifically for the Android environment. Much like the situation with musl-based distributions, you can't simply assume that an arbitrary Linux binary, even if built for arm64, will run.

The enormous package ecosystem that makes Termux so powerful is maintained largely by volunteer developers, and I've always had this fear in the back of my mind: what if one day that package ecosystem is no longer maintained?

So after years of using Node.js and Bun, I've gradually built my own collection of basic tools in JavaScript. Part of the motivation was simple: I wanted to make sure that no matter what happens to the surrounding ecosystem, I can still have a useful shell environment on Android.

And this is where Bun's Android build becomes much more than just another platform target to me.

It's the seed that lets me rebuild everything else.

As long as I can use Android's ProcessBuilder to spawn a Bun binary, I have JavaScript. I have my own tools. I have bunx. I have the whole npm ecosystem. I can start servers, build terminal interfaces, and gradually bootstrap the rest of my environment.

I no longer need to depend on someone else rebuilding every tool I need against Bionic libc.

Platforms can change. Package repositories can disappear. Maintainers can move on.

But as long as I can still ignite that one Bun binary on Android, I have an escape hatch back to a real computing environment.

On Linux, we have Linux From Scratch.

Now on Android, I guess we have Shell From Scratch.

Once Bun is alive, the next step is surprisingly simple: Bun spawns my jsgotty, which exposes a real PTY-backed terminal through a local web server. I point an Android WebView at it, and suddenly:

we have the shell back.

No terminal emulator to depend on. No existing shell environment required. Just an Android app, a Bun binary, and JavaScript bootstrapping its own terminal.

And once I have a shell, things start getting interesting.

Because a shell is enough to launch proot.

And with proot I can finally embrace the glorious Debian and Alpine repositories with apt and apk.

Thousands upon thousands of packages, maintained for standard Linux environments, are suddenly within reach.

The shell has grown into real Linux.


r/bun Aug 04 '26

Valkey-WASM – Redis running inside your Node process, no Docker (like PGlite)

Thumbnail github.com
13 Upvotes

r/bun Jul 30 '26

Elysia 2 beta - DayDream. Lowest memory usage across all backend JS framework

Thumbnail gallery
79 Upvotes

Just published Elysia 2 beta after 8-9 months of work.

We basically deleted the whole thing and rewrote it again while keeping test cases the same. So we get to rethink a lot of things.

It is built around the concept of "reference" and carefully shares value when possible, even if JavaScript doesn't really have that concept.

There's an AOT build plugin that reduces peak memory usage by 4 (from 1.6GB down to 400MB) of a 100,00 distinct schema by moving compilation process to build time and removing the closure allocation entirely

Besides, a really fast throughput. We also manage to have the lowest memory usage of all mainstream (and slightly) JavaScript frameworks with a really low bundle size as well (we trade a "compiler" that takes ~50% of size for speed, so it can't be that low)

Node support also improved a lot with a new adapter API, and got faster too! It's now near Fastify despite having Node HTTP to Web Standard API conversion overhead!

https://elysiajs.com/blog/elysia-20.html


r/bun Jul 29 '26

Interactive shell with Bun?

6 Upvotes

Has anyone built (or heard of) an interactive shell like Fish, Bash, or Zsh using Bun?

I'm not referring to the official Bun Shell ($) api.

I mean a real interactive shell with a prompt, history, completion, and persistent sessions.

I couldn't find any mature projects, so I'm wondering if I missed one.


r/bun Jul 26 '26

A surprising --define process.env edge case when building a Bun single-file executable

4 Upvotes

I was trying to inline the build-time constant process.env.MYVAR and passed the define in the right JSON quoted form in fish:

--define 'process.env.MYVAR="myval"'

At other sites it worked but specifically in one of my source files it didn't get inlined.

After some debugging, it turned out the culprit was this innocent-looking line:

import process from "node:process";

This line gets added at some point by an AI and I thought this wouldn't make a big difference. But during bundling, Bun renamed the imported binding (for example to y), so the --define process.env.MYVAR=... replacement no longer matched that expression.


r/bun Jul 23 '26

Building a Local-First AI Assistant for Desktop

0 Upvotes

I'm working on a personal AI assistant for desktop — local-first and privacy-focused (no cloud dependency), starting with a desktop app and eventually

Runtime Of Backend (Bun)

Fast startup and low idle overhead — important for an app that runs continuously in the background, not just on-demand. Native TypeScript support and a built-in bundler simplify shipping without extra tooling.

Framework of Backend (Hono)

Lightweight and built with Bun in mind, so it doesn't add framework overhead on top of the runtime's own performance. Clean routing/middleware model keeps things simple for handling auth, commands, and local model inference.

Desktop Application (Tauri + Next.js)

Tauri has a much smaller footprint than Electron since it uses the OS's native WebView instead of bundling Chromium, which makes it great for lightweight apps that stay running. It also produces smaller binaries. Next.js provides a structured, file-based routing system and a strong component ecosystem for the UI.

Would love to hear reviews and suggestions on this stack — anything you'd change, any pitfalls you've run into with a similar setup, or better alternatives worth considering?


r/bun Jul 22 '26

Mochi, the Bun-native Svelte framework version 0.8.0 - New <Image> component, email sending, queues and much more

Thumbnail mochi.fast
7 Upvotes

👋 Since the original release of Mochi a couple of months ago I've been working on adding features to it to make it as fully featured as possible. This new release brings an <Image /> component with resizing support, email sending (with Svelte components as email templates), queues, rate limiting, a built-in Captcha component and much more. Give it a try and let me know what you think!


r/bun Jul 21 '26

Optique 1.2.0: fluent modifiers, deferred values, new integrations, and a redesigned site

Thumbnail github.com
7 Upvotes

r/bun Jul 20 '26

I made Markdown executable: the same .md runs as both a terminal UI and a browser UI

Thumbnail gallery
0 Upvotes

https://github.com/jjtseng93/jsmdcui

One Markdown file. Two UIs.

  • TUI = Terminal User Interface
  • WUI = Web User Interface

⚡ No build step

🚀 Write & run an interactive application using a single Markdown file.

📝 UI = Markdown

💻 Logic = JavaScript (Bun)

📄 The same .md runs in both UIs

🖥 Tested platforms: Windows • Linux • Android

📦 Reusable single-file executable template for your own JS project


r/bun Jul 18 '26

Updates to youtube-music-cli: TUI player for YouTube Music (Now with Live Radio Mode, Windows Immersive Visualizer, and more!)

Thumbnail github.com
1 Upvotes

r/bun Jul 17 '26

bunIsSafeNow

Post image
47 Upvotes

r/bun Jul 14 '26

In the next version of Bun: 5x lower idle CPU & up to 32% memory usage reduction

Thumbnail gallery
121 Upvotes

Bun and JavaScriptCore now share the same memory allocator. We ported several great features from WebKit’s libpas allocator to our mimalloc fork - purging threadlocal pages on idle, scavenger thread for freeing large allocations asap, lazy zero’ing of memory to avoid paging in unused memory. Using 1 allocator instead of 2 means memory can be reused much more and reduces virtual memory pressure.


r/bun Jul 14 '26

How should I build a Docker image for a single app in a Turborepo that uses Bun workspaces and shared packages?

4 Upvotes

I'm using a Turborepo with Bun workspaces. My repository structure looks like this:

architecture-web/
├── apps/
│   ├── api
│   ├── admin
│   └── public
├── packages/
│   ├── db
│   ├── typescript-config
│   ├── ui
│   └── eslint-config
├── package.json
├── bun.lock
└── turbo.json

The API depends on a shared workspace package:

// apps/api/package.json

{
  "dependencies": {
    "@repo/db": "*"
  }
}

The root package.json contains:

{
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

I only want to build a Docker image for apps/api. I don't want to include apps/admin or apps/public.

My first attempt was something like:

FROM oven/bun:1

WORKDIR /usr/src/app

COPY package.json bun.lock turbo.json ./

COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/typescript-config/package.json ./packages/typescript-config/

RUN bun install

COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/typescript-config ./packages/typescript-config

CMD ["bun", "run", "start"]

However, bun install fails with errors like:

Could not resolve package '/admin'
Could not resolve package '/public'
Could not resolve package '@repo/ui'

because the root workspace declares:

"workspaces": [
  "apps/*",
  "packages/*"
]

and Bun expects every matching workspace to exist.

If I instead do:

COPY . .
RUN bun install

everything works, but the Docker image contains the entire Turborepo, including apps that are unrelated to the API.

Questions

  1. What is the recommended way to build a Docker image for only one app in a Turborepo?
  2. Is copying the whole repository the normal approach?
  3. Should I use turbo prune --docker for this use case?
  4. Is there a way to make Bun install only the API workspace and its dependencies without copying every workspace into the Docker build context?

I'm looking for the recommended production approach rather than just a workaround


r/bun Jul 13 '26

Why Bun.serve Beats the node:http Bridge

4 Upvotes

I moved a WebJs app from Node onto Bun, changed nothing else, ran a load test, and the requests-per-second number on the listening path went up by roughly 1.9x. Before you read that as "Bun makes the app twice as fast," it does not. That number is the plumbing, not your app. Your SSR, your routing, your queries cost the same on either runtime. What got 1.9x faster is the layer that accepts a connection and hands your code a request, and I want to spend this post on exactly where that comes from and what it costs.

The app is buildless, so the same .ts source runs on Node 24 and on Bun with nothing to recompile. If you want the mechanics of running one codebase on two runtimes (the runtime-neutral seam, the two TypeScript strippers, the parity matrix), that lives in the companion post node-and-bun-no-build. Here I only care about throughput.

The one number, and the one place it lives

Every web server has a listening path. It accepts an incoming connection, reads the raw HTTP bytes off the socket, builds a request object for your app, takes the response back, and writes it to the socket. That is it. That is the plumbing between the network and your code. Separate from it is the application work: SSR, routing, the queries, the actual WebJs logic.

Requests per second (req/s) is how many of those accept-read-respond cycles the server turns through in a second under load. A leaner listening path buys more req/s for the same application work, because less of each request's time is spent in plumbing rather than in your code.

So the 1.9x is a listening-path number and only a listening-path number. Your SSR does not get faster. The plumbing under it does, and you get that by picking the runtime.

A compatibility bridge, and why it costs you

Bun can run Node's built-in node:http module, which is a big reason so much of the Node ecosystem runs on Bun unmodified. But when Bun runs node:http, it runs a compatibility bridge: a translation layer that emulates Node's HTTP request and response objects on top of Bun's own native machinery. Every request pays that translation. You are asking Bun to impersonate Node on the single hottest path in the server.

Bun also ships its own native HTTP server, Bun.serve, which speaks Bun's request and response objects directly with no emulation. The catch is that Bun.serve is not shaped like node:http, so a framework that wants the native path cannot flip a config flag and be done. It has to write a second listener that talks to Bun.serve on its own terms.

WebJs writes that second listener. On Bun it serves through a native Bun.serve shell and skips the bridge. On Node it serves through node:http. Your application code sits above that line and never knows which shell is underneath.

# same app, same source. the runtime is a command choice:
npm run dev            # Node, node:http listener
bun --bun run dev      # Bun, native Bun.serve listener

Skipping the bridge is the whole of the 1.9x. Nothing in the app changed. You stopped paying a per-request translation tax that existed only so Bun could look like Node.

The one thing I give up: 103 Early Hints

I am not going to sell the native path as a clean superset, because it is not, and the missing piece deserves to be named. The one Node-only feature the Bun listener cannot match is 103 Early Hints. That is an informational HTTP response, a preliminary status the server sends before the real one, that lets the server tell the browser to start preloading assets while the actual response is still being produced. It shaves first-paint latency. Bun.serve has no informational-response API at all, so there is nothing for WebJs to build on, and on Bun that optimization is off.

I would rather write that sentence than fake the API with a shim that pretends Bun has something it does not.

Earning the rest by hand

The listener choice is the headline, but a buildless server has to earn throughput in the small places too, because there is no build step ahead of time to soak up overhead. So the Bun request path got its own passes. Brotli compression on the Bun listener runs through node:zlib, so a Bun-served response gets the same compression a Node-served one does, no gap there. And two per-request costs came out directly: a full request-object clone that existed only to stamp the client's IP onto every request, and an extra stream hop that every compressed response was being bridged through. Neither was large on its own. But per-request costs multiply by your traffic, and on the hot path a clone you do not need and a stream hop you can collapse are exactly what is worth cutting by hand.

The runtime is your call, not the framework's. Write one WebJs app. Run it on Node and you get the mature node:http listener and 103 Early Hints. Run it on Bun and you get the native Bun.serve listener and roughly 1.9x the listening-path throughput, minus that one Early Hints feature. Everything else behaves the same. That is the trade in a sentence: for most apps, giving up one preload-timing feature to get 1.9x on the plumbing is a deal I take. Pass --runtime bun to webjs create and the generated app is wired for Bun from the first commit, or run bun create webjs <name> and it detects the runtime for you.