beta

uWS Server

uWebSockets.js HTTP server adapter for maximum throughput.

@miiajs/uws-server provides a high-performance HTTP server adapter using uWebSockets.js - a C++ networking library that is significantly faster than Node.js's built-in http module.

Installation

bun add @miiajs/uws-server uWebSockets.js@uNetworking/uWebSockets.js#v20.69.0

uWebSockets.js is installed directly from GitHub - the package is not published to npm.

Usage

import { Miia } from '@miiajs/core'
import { serve } from '@miiajs/uws-server'

const app = new Miia().register(AppModule)

await app.listen(3000, serve)

With custom hostname:

await app.listen(3000, '0.0.0.0', serve)

How it works

The adapter converts uWebSockets.js request/response objects to the Web Standard Request/Response API that MiiaJS expects. Since uWS HttpRequest is only valid synchronously, all metadata (method, URL, headers) is captured before any async gap.

Large request bodies are bridged to a ReadableStream and read at the handler's pace: once the stream's queue passes bodyHighWaterMark the socket is paused, and it resumes as the handler drains. A slow storage target therefore slows the upload down instead of filling memory. Small bodies with a declared Content-Length never reach that path - uWS accumulates them natively in C++.

Ending the response disposes an unread request body. The stream is errored, not closed, so a body read that was started and never awaited rejects where it used to resolve - an unawaited ctx.json() followed by a response leaves an unhandled rejection behind. A body cut off mid-flight must never look complete, which is why it errors rather than closing.Whatever the client still has to send is then read and discarded, which is what keeps the connection reusable: bounded in memory, but unbounded in bytes read, with uWS's idle timeout as the only backstop. Answering early does not stop a large upload, it only stops paying for it in memory.

Performance modes

Optimized mode (default)

Minimizes allocations on the hot path:

  • Lazy Request Proxy - lightweight object instead of new Request(). Method, URL, headers, and body are resolved only on first access.
  • Lightweight Headers - linear scan over header pairs instead of constructing a Headers object. uWS provides lowercase keys natively, so lookups are direct string comparisons.
  • Body Buffering - small POST bodies (Content-Length ≤ bufferThreshold) are buffered and parsed directly, bypassing ReadableStream and Request creation. Large or chunked bodies fall back to streaming.
  • LightResponse Cache - simple responses (string, null, Uint8Array) store a [status, body, headers] tuple without creating a real Response object.
  • Corked Writes - batches header + body writes into a single syscall via res.cork().
  • Sync Fast Path - synchronous handlers bypass Promise allocation entirely.

Native mode

const server = await serve({
  fetch: app.fetch,
  port: 3000,
  mode: 'native',
})

Full Web API compliance with standard Request and Response objects. Use when strict spec conformance is needed.

Standalone usage

import { serve } from '@miiajs/uws-server'

const server = await serve({
  fetch: (req) => new Response('Hello, World!'),
  port: 8080,
})

await server.close()

Options

interface ServeOptions {
  fetch: (req: Request) => Response | Promise<Response>  // Required
  port?: number              // Default: 3000
  hostname?: string          // Default: '0.0.0.0'
  mode?: 'optimized' | 'native'  // Default: 'optimized'
  bufferThreshold?: number   // Default: 102400 (100KB)
  bodyHighWaterMark?: number // Default: 262144 (256KB)
  maxBodySize?: number | false   // Default: 1048576 (1MB)
}

The bufferThreshold controls the body buffering optimization in optimized mode. POST/PUT/PATCH bodies with a known Content-Length up to this size are buffered in memory for fast json()/text() access. Bodies without Content-Length or larger than the threshold use ReadableStream for streaming.

The bodyHighWaterMark is how many bytes a streamed request body may queue before the socket is paused; it resumes as the handler drains the stream, so the client uploads at the handler's pace instead of filling memory. It applies to the streaming path only - buffered bodies are bounded by bufferThreshold by construction. Values must be integers of at least 1; anything else makes serve() reject.

The pause is advisory: uSockets keeps reading from the socket until the kernel runs dry inside one poll event, so the queue overshoots by whatever the client already had in flight - measured at 1.4 MB after a pause that fired at 16 KB. Size capacity against bodyHighWaterMark plus roughly 1.5 MB resident per concurrent upload. Measured at 64 KB, 256 KB and 1 MB, the value made no difference to throughput on a single 160 MB upload or on ten concurrent ones, so the default sits in the middle of a flat range - very small values buy nothing but pause/resume churn.

bodyHighWaterMark is not reachable through app.listen(port, serve): Miia passes a fixed set of options to the adapter callback, and this is not one of them. Wrap the call to set it, the same way maxBodySize is overridden below:

await app.listen(3000, (info) => serve({ ...info, bodyHighWaterMark: 1024 * 1024 }))

The maxBodySize caps request bodies. A request whose declared Content-Length exceeds the cap gets an immediate 413 JSON response - the handler never runs. Chunked bodies (no Content-Length) are capped in-stream: the body stream rejects with an Error named 'PayloadTooLargeError', which @miiajs/core maps to a 413 automatically. Non-Miia consumers should catch this error by name. Pass false to disable the cap.

When used via app.listen(port, serve), Miia passes its computed ceiling (max of maxBodySize and all @BodyLimit values) automatically. To override it for the adapter, wrap the call:

await app.listen(3000, (info) => serve({ ...info, maxBodySize: 5 * 1024 * 1024 }))

When to use

Choose @miiajs/uws-server when:

  • Maximum throughput is critical
  • You're running on Node.js and need higher performance than the built-in http module
  • You're comfortable with native binary dependencies

Choose @miiajs/node-server when:

  • You prefer zero native dependencies
  • You need broader platform compatibility
  • Performance is sufficient with the Node.js adapter

On Bun and Deno, no adapter is needed - app.listen() auto-detects the runtime.

Testing

@miiajs/uws-server is exercised directly via serve(). Tests must run under Node.js - the native uWS binary does not load on Bun or Deno.

// node --experimental-strip-types --no-warnings --test
import { afterEach, describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { serve } from '@miiajs/uws-server'

let server: { close(): Promise<void> }

afterEach(async () => {
  if (server) await server.close()
})

it('handles GET requests', async () => {
  server = await serve({
    port: 19234,
    fetch: () => Response.json({ ok: true }),
  })

  const res = await fetch('http://localhost:19234/')
  assert.equal(res.status, 200)
  assert.deepEqual(await res.json(), { ok: true })
})

For applications that wire the adapter through app.listen(port, host, serve), prefer the TestApp pattern from Testing - it runs handlers without binding a port and works under any runtime.