SvelteKit Remote Functions and Observability

Practical patterns for typed remote calls, server contexts, and adding OpenTelemetry traces to SvelteKit apps.

Prerequisites

Enable experimental remote functions and optional async component support in your SvelteKit config, and use TypeScript for end‑to‑end typing. Remote functions are available behind an experimental flag in SvelteKit; treat them as server‑side functions that compile to HTTP endpoints.

// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default {
  kit: {
    adapter: adapter(),
    experimental: { remoteFunctions: true }
  },
  compilerOptions: { experimental: { async: true } }
};

What remote functions are and when to use them

Remote functions let you call server code from the client with generated, type‑safe wrappers instead of hand‑written endpoints. They run on the server and can access secrets, DB clients, and server‑only modules while preserving a simple call syntax on the client. Use them when you want concise client→server calls with shared types and minimal boilerplate.

Simple remote function example

Create a file under a route with a named export; SvelteKit exposes a client wrapper automatically.

// src/routes/api/todos.remote.ts
import { db } from '$lib/server/db';
export async function getTodos(userId: string) {
  return await db.select().from('todos').where('user_id', userId);
}

Call from a component using the generated wrapper:

// src/routes/+page.svelte

Because the function executes on the server, you can safely import server‑only modules such as database clients or environment variables.


Typed contexts and sharing server state

Typed contexts let you pass structured, typed dependencies (for example, a DB client or request metadata) into remote functions and other server code. Define a central type for your server context and ensure your adapters and hooks populate it consistently.

Define a server context type

// src/types/server.d.ts
import type { Database } from './db-types';
declare global {
  namespace App {
    interface Locals {
      db: Database;
      user?: { id: string; roles: string[] };
      traceId?: string;
    }
  }
}

Populate context in hooks.server.ts

// src/hooks.server.ts
import { initDb } from '$lib/server/db';
export async function handle({ event, resolve }) {
  event.locals.db = initDb();
  // optionally attach a trace id for observability
  event.locals.traceId = crypto.randomUUID();
  return resolve(event);
}

Remote functions can then read from event.locals (or the typed context) to access shared services without reinitializing them per call.


Adding OpenTelemetry traces to SvelteKit

Instrumenting SvelteKit with OpenTelemetry gives you traces that connect incoming HTTP requests, remote function executions, and downstream DB calls. Recent SvelteKit releases added first‑class hooks and patterns that make adding OpenTelemetry straightforward.

High‑level approach

1. Initialize SDK

Start an OpenTelemetry SDK in a server entry point and configure exporters (OTLP, Jaeger, or your APM).

2. Create instrumentation

Instrument HTTP server, fetch, and database clients so spans are created automatically for requests and remote calls.

Minimal instrumentation.server.ts

// src/instrumentation.server.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_URL }),
  instrumentations: [getNodeAutoInstrumentations()]
});

export async function initTracing() {
  await sdk.start();
  return sdk;
}

Call initTracing() early in your server startup (for example, in your adapter entry or a top‑level server module). A community example repository demonstrates wiring remote functions and traces together.

Propagating trace context into remote functions

Attach the current trace id or active span context to event.locals in your handle hook so remote functions can create child spans that link to the incoming request.

// src/hooks.server.ts (snippet)
import { trace, context } from '@opentelemetry/api';
export async function handle({ event, resolve }) {
  const span = trace.getTracer('app').startSpan('http.request', { attributes: { path: event.url.pathname } });
  event.locals.traceSpan = span;
  try {
    return await context.with(trace.setSpan(context.active(), span), () => resolve(event));
  } finally {
    span.end();
  }
}

Inside a remote function, create a child span for the operation and record attributes such as user id, DB query time, and error details.

// src/routes/api/todos.remote.ts (instrumented)
import { trace } from '@opentelemetry/api';
export async function getTodos(userId: string, event) {
  const tracer = trace.getTracer('todos');
  return tracer.startActiveSpan('getTodos', async (span) => {
    span.setAttribute('user.id', userId);
    try {
      const rows = await event.locals.db.query('SELECT * FROM todos WHERE user_id = $1', [userId]);
      span.setAttribute('db.rows', rows.length);
      return rows;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2, message: String(err) });
      throw err;
    } finally {
      span.end();
    }
  });
}

Debugging, testing, and common pitfalls

Tip: Run traces locally with a lightweight collector (OTLP/Jaeger) and use sampling rules to avoid noise while you iterate.

Common pitfalls

  • Uninitialized context — forgetting to populate event.locals in some code paths causes runtime errors.
  • Blocking startup — starting the OpenTelemetry SDK synchronously can delay server boot; initialize early but non‑blocking where possible.
  • Excessive spans — instrument only the operations you need; high cardinality attributes increase storage costs.

Testing traces

Write integration tests that assert spans are created for key flows. Use an in‑memory exporter during tests to inspect spans without external dependencies.

// test helper (pseudo)
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
const exporter = new InMemorySpanExporter();
// attach exporter to tracer provider in test setup and assert exporter.getFinishedSpans()

Production checklist

-Enable remoteFunctions— confirm flag in svelte.config.js.

-Typed Locals— declare App.Locals and populate in hooks.

-Tracing init— start OpenTelemetry SDK early and configure exporter.

-Span propagation— attach trace context to event.locals and create child spans in remote functions.

-Sampling & retention— set sampling and retention to control costs.

-Observability dashboards— map traces to service and DB metrics for quick root cause analysis.