Troubleshooting
“WebSocket works in production but not in dev”
You need the Vite plugin. Without it, there’s no WebSocket server running during npm run dev.
vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import uws from 'svelte-adapter-uws/vite';
export default {
plugins: [sveltekit(), uws()]
}; Also make sure ws is installed:
npm install -D ws “Cannot read properties of undefined (reading ‘publish’)”
This means event.platform is undefined. Two possible causes:
Cause 1: Missing Vite plugin in dev mode
Same fix as above - add uws() to your vite.config.js.
Cause 2: Calling platform on the client side
event.platform only exists on the server. If you’re calling it in a +page.svelte or +layout.svelte file, move that code to +page.server.js or +server.js.
// WRONG - +page.svelte (client-side)
platform.publish('todos', 'created', todo);
// RIGHT - +page.server.js (server-side)
export const actions = {
create: async ({ platform }) => {
platform.publish('todos', 'created', todo);
}
}; “WebSocket connects but immediately disconnects (and keeps reconnecting)”
Your upgrade handler is returning false, which rejects the connection with 401. The client store’s auto-reconnect then tries again, gets rejected again, and so on.
To debug, enable debug mode on the client:
import { connect } from 'svelte-adapter-uws/client';
connect({ debug: true }); Then check the browser’s Network tab → WS tab. You’ll see the upgrade request and its 401 response.
Common causes:
- The session cookie isn’t being set (check your login action)
- The cookie name doesn’t match (
cookies.sessionvscookies.session_id) - The session expired or is invalid
sameSite: 'strict'can block cookies on cross-origin navigations - try'lax'if you’re redirecting from an external site
Terminal close codes
To stop the retry loop when credentials are permanently invalid, close the WebSocket with a terminal close code from inside your open or message handler. The client will not reconnect on these codes:
| Code | Meaning |
|---|---|
1008 | Policy Violation (standard) |
4401 | Unauthorized (custom) |
4403 | Forbidden (custom) |
// src/hooks.ws.js
export async function open(ws, { platform }) {
const userData = ws.getUserData();
if (!userData.userId) {
ws.close(4401, 'Unauthorized'); // client will not retry
return;
}
} When the server closes with code 4429, the client treats it as a rate limit signal and backs off more aggressively before retrying.
“WebSocket doesn’t work with npm run preview”
This is expected. SvelteKit’s preview server is Vite’s built-in HTTP server - it doesn’t know about WebSocket upgrades. Use node build instead:
npm run build
node build “Could not load uWebSockets.js”
uWebSockets.js is a native C++ addon. It’s installed from GitHub, not npm, and needs to compile for your platform.
# Make sure you're using the right install command (no uWebSockets.js@ prefix)
npm install uNetworking/uWebSockets.js#v20.60.0 On Windows: Make sure you have the Visual C++ Build Tools installed. You can get them from the Visual Studio Installer (select “Desktop development with C++”).
On Linux: Make sure build-essential is installed:
sudo apt install build-essential On Docker: Use a Trixie-based image with git:
FROM node:22-trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* “I can’t see what’s happening with WebSocket messages”
Turn on debug mode. It logs every WebSocket event to the browser console:
<script>
import { connect } from 'svelte-adapter-uws/client';
// Call this once, anywhere - it's a singleton
connect({ debug: true });
</script> You’ll see output like:
[ws] connected
[ws] subscribe -> todos
[ws] <- todos created {"id":1,"text":"Buy milk"}
[ws] disconnected
[ws] resubscribe -> todos On the server side, set the WS_DEBUG environment variable:
WS_DEBUG=1 node build “Messages are arriving but my store isn’t updating”
Make sure the topic names match exactly between server and client:
// Server
platform.publish('todos', 'created', todo); // topic: 'todos'
// Client - must match exactly
const todos = on('todos'); // 'todos' - correct
const todos = on('Todos'); // 'Todos' - WRONG, case sensitive
const todos = on('todo'); // 'todo' - WRONG, singular vs plural “How do I see what the message envelope looks like?”
Every message sent through platform.publish() or platform.topic().created() arrives as JSON with this shape. The envelope is constructed with string concatenation for speed, but topic and event are validated first - if either contains a quote, backslash, or control character, the call throws instead of producing malformed JSON:
{
"topic": "todos",
"event": "created",
"data": { "id": 1, "text": "Buy milk", "done": false }
} The client store parses this automatically. When you use on('todos'), the store value is:
{ topic: 'todos', event: 'created', data: { id: 1, text: 'Buy milk', done: false } } When you use on('todos', 'created'), you get the payload wrapped in { data }:
{ data: { id: 1, text: 'Buy milk', done: false } } “WebSocket works locally but not behind nginx/Caddy”
Your reverse proxy needs to forward WebSocket upgrade requests. Here’s a complete nginx config that handles both your app and WebSocket:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# WebSocket - must be listed before the catch-all
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Everything else - your SvelteKit app
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} Then run your app with:
PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=host ADDRESS_HEADER=x-forwarded-for node build For Caddy, it just works - Caddy proxies WebSocket upgrades automatically, no special config needed:
example.com {
reverse_proxy localhost:3000
} “I want to use a different WebSocket path”
Set it in both the adapter config and the client:
svelte.config.js
adapter({
websocket: {
path: '/my-ws'
}
}) Client
import { connect } from 'svelte-adapter-uws/client';
connect({ path: '/my-ws' }); Or if you’re using on() directly (which auto-connects), call connect() first:
<script>
import { connect, on } from 'svelte-adapter-uws/client';
// Set the path before any on() calls
connect({ path: '/my-ws' });
const todos = on('todos');
</script> Was this page helpful?