---
title: "Startup"
description: "instrumentation.ts runs once, before anything else."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.rsc-kit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Startup

Some setup belongs to the process, not to a page: validating the
environment, configuring a shared package, warming a connection. Put it in
`src/instrumentation.ts` and it runs once — before any page module
evaluates, and before the first request.

```ts title="src/instrumentation.ts"
// Runs at import, before any page: a bad variable stops the server here.
import './env'

// Anything asynchronous the app needs before it serves. Called once per
// process, at startup; the first render waits for it, later ones do not.
export async function register() {
  await db.connect()
}
```

The name is Next's, so a reader arriving from there knows what it is. Both
halves are optional: a file with only imports is a bootstrap, a file with
only `register()` is a hook.

## Why it is a framework file

Two things make this a file the framework has to know about rather than a
module the app imports.

**Import order.** A page that configures a shared package at import time —
a logger, an ORM, a metrics client — runs before any module the app could
put in front of it, because the generated entry imports the pages. The entry
imports `instrumentation.ts` *first*, so whatever it sets up is set up when
the first page module evaluates. Importing a bootstrap module from every
file that needs it works until someone forgets, and the failure is a page
that throws "not configured" for the one visitor who reached it first.

**Before the first request.** `register()` is **called once**, and every
entry point a render can come through — the built server, the dev server,
the prerender, a route's middleware check, a server action, an api route —
waits on that one call rather than making its own. After startup it has long
since finished, so a request waits on nothing: it checks a promise that has
already settled. That is how "before the first render" holds without the app
knowing the list of entry points, and without a request ever paying for it.

## When it runs

| where | when |
| --- | --- |
| a server (`bun`, `node`, any long-lived preset) | at startup, before the server reports itself up |
| a Worker | at the isolate's first request — there is no startup, and a binding is only readable once a request has arrived |
| `vite dev` | when the dev server first evaluates the app, and again when the file or anything it imports changes |
| `vite build` | before the first page is prerendered |

**Once per process, not once per request.** Measured on a built server and on
a compiled binary, with a `register()` that logs: one call before the first
request, and still one after a stream of them.

A `register()` that resolved is not called again. One that *rejected* is the
exception: the next request calls it again, so a database that was not up yet
is asked again rather than leaving the process permanently refusing. On a
server that case is rare in practice — a startup failure exits the process,
below.

On a server, a failure at startup — the file throwing at import, or
`register()` rejecting — is reported and the process exits. A server that
could not bootstrap has nothing correct to serve, and a health check that
passed on a server about to fail its first visitor is worse than one that
never passed. The dev server stays up and reports the error on the page.

## Shutting down

Export `shutdown()` beside `register()` to close what `register()` opened.
A built server calls it when it is stopped, after in-flight requests have
drained, and then exits:

```ts title="src/instrumentation.ts"
export async function register() {
  await db.connect()
}

export async function shutdown() {
  await db.end()
}
```

It is bounded by `RSC_SHUTDOWN_TIMEOUT` (10 seconds), and one that throws is
reported rather than holding the process. The server exits whether or not
you export it; see [Stopping gracefully](/hosts/deployment/#stopping-gracefully).
Not called on a Worker, which has no process to stop, or by the dev server.

## Environment validation

The scaffold writes this file when the project validates its environment,
and the only line it needs is the import: `src/env.ts` refuses at import, and
importing it here is what makes a missing variable the server's failure at
startup rather than a visitor's three calls later.

The build prerenders pages, so it runs the bootstrap too. A build machine
without the production variables sets `SKIP_ENV_VALIDATION=1`, which the
generated schema honours; the server that runs the build validates at
startup regardless.

## On a Worker

Read bindings inside `register()`, not at the top of the module. A Worker
evaluates the module before any request exists, and `process.env` is empty
until Nitro maps the first request's bindings onto it — so a top-level
`process.env.DATABASE_URL` is `undefined` at exactly the moment it looks
like it should not be. Inside `register()`, which runs at the first request,
it is there.

```ts
export async function register() {
  const url = process.env.DATABASE_URL   // readable here on every host
  await db.connect(url)
}
```

The same file works unchanged on a server, where both moments have the
environment.

Source: https://docs.rsc-kit.dev/guides/instrumentation/index.mdx
