Configuration

Adapter options

adapter({
  out: 'build',
  precompress: true,
  envPrefix: '',
  healthCheckPath: '/healthz',
  websocket: true // or false, or an options object
})
OptionDefaultDescription
out'build'Output directory
precompresstrueGenerate brotli/gzip for static files
envPrefix''Prefix for environment variables
healthCheckPath'/healthz'Health check endpoint (set to false to disable)
websocketfalseEnable WebSocket support (true, false, or options object)

WebSocket options

Pass an object instead of true for fine-grained control:

adapter({
  websocket: {
    path: '/ws',
    handler: './src/lib/server/websocket.js',
    maxPayloadLength: 16 * 1024,
    idleTimeout: 120,
    maxBackpressure: 1024 * 1024,
    compression: false,
    sendPingsAutomatically: true,
    upgradeTimeout: 10,
    upgradeRateLimit: 10,
    upgradeRateLimitWindow: 10,
    allowedOrigins: 'same-origin'
  }
})
OptionDefaultDescription
path'/ws'WebSocket endpoint path
handlerauto-discoverPath to custom handler module. Auto-discovers src/hooks.ws.js if omitted
maxPayloadLength16384Max message size in bytes. Connections sending larger messages are closed
idleTimeout120Seconds of inactivity before the connection is closed
maxBackpressure1048576Max bytes of backpressure per connection (1 MB). Lower for many slow consumers
compressionfalsePer-message deflate compression
sendPingsAutomaticallytrueKeep-alive pings
upgradeTimeout10Seconds before an async upgrade handler is rejected with 504. Set to 0 to disable
upgradeRateLimit10Max WebSocket upgrade requests per IP per window. Prevents connection flood attacks
upgradeRateLimitWindow10Sliding window size in seconds
allowedOrigins'same-origin''same-origin', '*', or an array of allowed origins

Environment variables

All set at runtime (node build), not build time. If you set envPrefix: 'MY_APP_', all variables are prefixed (e.g. MY_APP_PORT).

VariableDefaultDescription
HOST0.0.0.0Bind address
PORT3000Listen port
ORIGIN(derived)Fixed origin (e.g. https://example.com)
SSL_CERT-Path to TLS certificate
SSL_KEY-Path to TLS private key
PROTOCOL_HEADER-Protocol detection header (e.g. x-forwarded-proto)
HOST_HEADER-Host detection header (e.g. x-forwarded-host)
PORT_HEADER-Port override header (e.g. x-forwarded-port)
ADDRESS_HEADER-Client IP header (e.g. x-forwarded-for)
XFF_DEPTH1Position from right in X-Forwarded-For
BODY_SIZE_LIMIT512KMax request body (supports K, M, G suffixes)
SHUTDOWN_TIMEOUT30Graceful shutdown wait in seconds
CLUSTER_WORKERS-Worker threads (auto for CPU count)
CLUSTER_MODE(auto)reuseport (Linux) or acceptor (other platforms)
WS_DEBUG-Set to 1 for structured WebSocket debug logging

Important: PROTOCOL_HEADER, HOST_HEADER, PORT_HEADER, and ADDRESS_HEADER are trusted verbatim. Only set these behind a reverse proxy that overwrites them on every request. If the server is directly internet-facing, use a fixed ORIGIN instead.

Examples

# Simple HTTP
node build

# Custom port
PORT=8080 node build

# Behind nginx
ORIGIN=https://example.com node build

# Behind a proxy with forwarded headers
PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build

# Native TLS
SSL_CERT=./cert.pem SSL_KEY=./key.pem node build

# Everything at once
SSL_CERT=./cert.pem SSL_KEY=./key.pem PORT=443 BODY_SIZE_LIMIT=10M SHUTDOWN_TIMEOUT=60 node build

TypeScript setup

Add the platform type to src/app.d.ts:

import type { Platform as AdapterPlatform } from 'svelte-adapter-uws';

declare global {
  namespace App {
    interface Platform extends AdapterPlatform {}
  }
}

export {};

Now event.platform.publish(), event.platform.topic(), etc. are fully typed.


Vite plugin

The Vite plugin is required when using WebSockets. It does two things:

  1. Dev mode - spins up a ws WebSocket server alongside Vite’s dev server, so event.platform and the client store work identically to production
  2. Production builds - runs your hooks.ws file through Vite’s pipeline so $lib, $env, and $app imports resolve correctly

Without it, event.platform won’t work in dev, and your hooks.ws file won’t be able to import from $lib or use $env variables.

// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import uws from 'svelte-adapter-uws/vite';

export default {
  plugins: [sveltekit(), uws()]
};

Changes to your hooks.ws file are picked up automatically - the plugin reloads the handler on save and closes existing connections so they reconnect with the new code. No dev server restart needed.

Note: The dev server does not enforce allowedOrigins. Origin checks only run in production. A warning is logged at startup as a reminder.


Health check endpoint

The adapter exposes a health check at /healthz by default. Set healthCheckPath to a different path or false to disable:

adapter({
  healthCheckPath: '/healthz' // default
  // healthCheckPath: false   // disable
})

Static file behavior

All static assets (from the client/ and prerendered/ output directories) are loaded once at startup and served directly from RAM. Each response automatically includes:

  • Content-Type - detected from the file extension
  • Vary: Accept-Encoding - required for correct CDN/proxy caching
  • Accept-Ranges: bytes - enables partial content requests
  • X-Content-Type-Options: nosniff - prevents MIME-type sniffing
  • ETag - derived from modification time and size; enables 304 Not Modified
  • Cache-Control: public, max-age=31536000, immutable - for versioned assets under /_app/immutable/
  • Cache-Control: no-cache - for all other assets (forces ETag revalidation)

Range requests (HTTP 206): Single byte ranges are supported (bytes=0-499, bytes=-500, bytes=500-). Multi-range requests are served as full 200 responses. Unsatisfiable ranges return 416. When a Range header is present, the response is always served uncompressed so byte offsets are correct. The If-Range header is respected.

Binary downloads: Files with extensions like .zip, .tar, .exe, .dmg, .pkg, .deb, .apk, .iso, .img, .bin automatically receive Content-Disposition: attachment so browsers prompt a download dialog.

Precompression: If precompress: true, brotli (.br) and gzip (.gz) variants are loaded at startup and served when the client’s Accept-Encoding includes br or gzip. Precompressed variants are only used when smaller than the original.


Graceful shutdown

On SIGTERM or SIGINT, the server:

  1. Stops accepting new connections
  2. Waits for in-flight SSR requests to complete (up to SHUTDOWN_TIMEOUT seconds)
  3. Emits a sveltekit:shutdown event on process
  4. Exits
process.on('sveltekit:shutdown', async (reason) => {
  console.log(`Shutting down: ${reason}`);
  await db.close();
});

Deployment

  • Docker: Use node:22-trixie-slim (glibc >= 2.38 required for the uWS native addon)
  • Build: npm run buildnode build
  • Clustering: Set CLUSTER_WORKERS=auto for multi-core, uses SO_REUSEPORT on Linux
  • OS tuning: Increase nofile ulimit to 65536+ for high connection counts
# Production with clustering and TLS
SSL_CERT=./cert.pem SSL_KEY=./key.pem CLUSTER_WORKERS=auto node build

Was this page helpful?