# FrontMcpInstance

> The class that builds and runs a server from its @FrontMcp configuration, over HTTP, stdio, a Unix socket, a serverless handler or in-process.

Source: https://frontmcp.dev/reference/sdk/frontmcp-instance

`FrontMcpInstance` builds a server from the configuration you give [`@FrontMcp`](https://frontmcp.dev/reference/sdk/frontmcp) and runs it. You rarely call it: importing a class decorated with `@FrontMcp` calls `FrontMcpInstance.bootstrap()`, which builds the server and starts an HTTP server. Its other static methods run the same configuration in other ways: over stdio, on a Unix socket, as a handler for a serverless platform or a Worker, or [in your own process](https://frontmcp.dev/reference/sdk/create).

```ts
await FrontMcpInstance.bootstrap(config)
```

---

## Reference

### `FrontMcpInstance.bootstrap(config)`

Call `bootstrap()` to start the HTTP server yourself, for example after connecting to a database. Set `serve: false` on `@FrontMcp`, so importing the class doesn't start a second server, and pass the same configuration:

```ts main.ts
import "reflect-metadata";
import { FrontMcp, FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";
import { connectToDatabase } from "./db";

@FrontMcp({ ...config, serve: false })
export default class HelpDeskServer {}

await connectToDatabase();
await FrontMcpInstance.bootstrap(config);
```

[See more examples below.](#usage)

Starting a server needs a real port and a Node process, so the Playground can't run `bootstrap()`, `createHandler()`, `runStdio()` or `runUnixSocket()`. What this page says about them was checked by running them in Node.

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `config` | `FrontMcpConfigInput` | The object you'd pass to `@FrontMcp`: [`info`, `apps`](https://frontmcp.dev/reference/sdk/frontmcp#options) and the rest. Not the decorated class: a class fails with a `ZodError`, [`Invalid input: expected object, received undefined`](#invalid-input-expected-object-received-undefined). |

#### Returns

A promise that resolves once the server is listening, and logs `MCP HTTP (Express) on 127.0.0.1:3000`. It rejects when the configuration is invalid or the server can't listen, for example `listen EADDRINUSE: address already in use 127.0.0.1:3000` when the port is taken.

`bootstrap()` returns nothing to stop the server with. The process ends it.

### What `@FrontMcp` does on import

When the decorated class is defined, `@FrontMcp` checks the configuration, records it on the class, and then, depending on the environment:

| Environment | What happens |
| --- | --- |
| `FRONTMCP_SCHEMA_EXTRACT` set | Nothing more. The configuration is only recorded. |
| `FRONTMCP_STDIO` set | [`runStdio(config)`](#frontmcpinstancerunstdioconfigorclass): serve one client over stdin and stdout. Nothing, with `serve: false`. |
| `FRONTMCP_SERVERLESS` set | [`createHandler(config)`](#frontmcpinstancecreatehandlerconfig), or `createFetchHandler(config)` if `FRONTMCP_WORKER` is set too. The handler is kept for [`getServerlessHandler()`](#getserverlesshandler-and-getserverlesshandlerasync). |
| None of those | `bootstrap(config)`, unless the configuration has `serve: false`. |

A flag counts as set when its value is `1` or `true`; `yes` or `on` don't count. `frontmcp build` relies on these: its Vercel and Lambda entry files set `FRONTMCP_SERVERLESS=1` before importing your server and export the handler from `getServerlessHandlerAsync()`, and its Cloudflare entry sets `FRONTMCP_WORKER=1` as well.

If starting fails, for example because the port is taken, nothing catches the error: Node reports it and the process exits with code 1.

`FRONTMCP_SCHEMA_EXTRACT` is for tools that need your configuration without running your server. `frontmcp build` uses it, and so does this site's Playground, which is why importing an example's `@FrontMcp` class here never opens a port. Read the recorded configuration with `getDecoratorConfig()`:

```ts
import { getDecoratorConfig } from "@frontmcp/sdk";
import HelpDeskServer from "./main";

const config = getDecoratorConfig(HelpDeskServer); // the parsed configuration, or undefined
```

### Static methods

| Method | Returns | Use it to |
| --- | --- | --- |
| `bootstrap(config)` | `Promise<void>` | Start the HTTP server. [See above.](#frontmcpinstancebootstrapconfig) |
| `createFetchHandler(config)` | `Promise<(request, ctx?, env?) => Promise<Response>>` | Serve MCP from a runtime that hands you a web `Request`: Cloudflare Workers, Deno, Bun, a framework's route. See [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler). |
| `createHandler(config)` | `Promise<unknown>`, an Express app | Serve MCP from a platform that calls a Node `(req, res)` handler, like Vercel or AWS Lambda. [See below.](#frontmcpinstancecreatehandlerconfig) |
| `createDirect(config, { app }?)` | `Promise<DirectMcpServer>` | Call tools, resources and prompts from your own code, with no transport. `app` picks an app's own endpoint, and a `workerEnv` in `config` sets the [bindings](https://frontmcp.dev/reference/sdk/create#passing-a-workers-bindings) its calls read (since 1.9.4). See [`create()` and `createDirect()`](https://frontmcp.dev/reference/sdk/create). |
| `runStdio(configOrClass)` | `Promise<void>` | Serve one local client over stdin and stdout. [See below.](#frontmcpinstancerunstdioconfigorclass) |
| `runUnixSocket(options)` | `Promise<{ close }>` | Serve over a Unix socket instead of a TCP port. [See below.](#frontmcpinstancerununixsocketoptions) |
| `createForGraph(config)` | `Promise<FrontMcpInstance>` | Build the server without serving it, to inspect it. [See below.](#inspecting-a-server-without-serving-it) |
| `createForCli(config)` | `Promise<FrontMcpInstance>` | The same, for the `frontmcp` CLI's own commands: it skips widget compilation and the event and elicitation stores to start faster. |

To connect an MCP client in memory instead, use [`connect()`](https://frontmcp.dev/reference/sdk/connect). It isn't a method of `FrontMcpInstance`.

Every method except `runStdio()` needs the configuration object. [`connect()`](https://frontmcp.dev/reference/sdk/connect) and `runStdio()` also accept the decorated class.

#### `FrontMcpInstance.createHandler(config)`

Builds the server without listening, and returns the Express app that would have listened, a Node `(req, res)` handler. Export it from the entry file of a platform that calls one:

```ts api/mcp.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "../src/config";

export default await FrontMcpInstance.createHandler(config);
```

It serves the same paths as `bootstrap()`, and works with `http.createServer(handler)` too. With `serve: false` in the configuration there's no Express app to return, and the promise resolves to `undefined`.

#### `FrontMcpInstance.runStdio(configOrClass)`

Serves one client over stdin and stdout, for clients that start your server as a subprocess, like Claude Desktop. It opens no port.

```ts stdio.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

FrontMcpInstance.runStdio(config);
```

- stdout carries the protocol, so from the moment you call it, `runStdio()` sends `console.log`, `console.info` and `console.debug` to stderr. A `console.log` before the call still writes to stdout and breaks the client.
- The promise resolves once the client is connected, not when it disconnects.
- On `SIGINT` or `SIGTERM` it shuts the server down and exits with code 0, within 5 seconds.
- Callers over stdio are anonymous: `this.auth.isAnonymous` is `true` and `this.auth.user.sub` is `""`.

It accepts the decorated class as well as the configuration. The class must be imported with `FRONTMCP_STDIO=1` already set, or importing it starts an HTTP server first.

#### `FrontMcpInstance.runUnixSocket(options)`

Serves over a Unix socket file instead of a TCP port, so only processes on the same machine that can open the file can connect:

```ts
const handle = await FrontMcpInstance.runUnixSocket({ ...config, socketPath: "/tmp/help-desk.sock" });
```

`options` is the configuration plus `socketPath` and an optional `sqlite`. `bootstrap()` does the same when the `FRONTMCP_DAEMON_SOCKET` environment variable names a socket path. On `SIGINT` or `SIGTERM` it [shuts down](#lifecycle-and-shutdown) like `bootstrap()`: it deletes the socket file, so new clients can't connect, lets requests in flight finish for up to 5 seconds, and exits with code 0.

`handle.close()` runs the same shutdown without exiting: it resolves once requests in flight have finished and the socket file is gone. (Before 1.9.3, `SIGTERM` exited at once and `close()` only deleted the socket file.)

#### `getServerlessHandler()` and `getServerlessHandlerAsync()`

With `FRONTMCP_SERVERLESS=1`, `@FrontMcp` builds a handler instead of listening. Import the decorated class, then get the handler:

```ts
import { getServerlessHandlerAsync } from "@frontmcp/sdk";
import "./main"; // defines the @FrontMcp class

const handler = await getServerlessHandlerAsync();
```

`getServerlessHandlerAsync()` waits until the handler is ready. `getServerlessHandler()` returns it at once, or `null` while it's still being built. Both throw what building threw. Without `FRONTMCP_SERVERLESS`, `getServerlessHandlerAsync()` throws [`Serverless handler is not initialized`](#serverless-handler-is-not-initialized).

### Instance members

`bootstrap()` builds an instance and starts it. `createForGraph()` returns one that's built and not started.

| Member | Description |
| --- | --- |
| `config` | The parsed configuration, with defaults filled in. |
| `getConfig()` | Returns `config`. |
| `ready` | A promise that resolves once the server is built: its apps, tools, resources and the rest. On failure, waits for cleanup of the partial server for at most 10 seconds before rejecting with the startup error. Cleanup can still be pending after that deadline. |
| `shutdown()` | `Promise<void>`. Waits for initialization to finish or fail, then shuts down and disposes every scope and stops the HTTP host. Repeated calls share the same promise. |
| `getScopes()` | The server's [scopes](https://frontmcp.dev/reference/sdk/scope): one for the whole server, or one per app with `splitByApp: true`. |
| `getPrimaryScope()` | The scope that `runStdio()` serves, and `createDirect()` and `connect()` without an `app`: the one holding every app that isn't `standalone`, `root`. With `splitByApp: true`, or when every app is `standalone`, the first scope. |
| `getAppScope(app)` | The scope an app has of its own, served at `<entryPath>/<app id>`: each app's with `splitByApp: true`, a `standalone` app's otherwise. `createDirect()` and `connect()` serve it when given that `app`. Throws `ScopeConfigurationError`, `No endpoint serves app "…" on its own`, for an app without one, naming the apps that have one. New in 1.9.3. |
| `start()` | Starts the HTTP server of a built instance, then runs the callbacks registered with `scope.onServerStarted()`. |
| `initialize()` | Builds the server. The constructor calls it and keeps the promise in `ready`; don't call it again. |

The constructor, `new FrontMcpInstance(config)`, takes a configuration that has already been parsed. Use the static methods, which parse it for you.

### Ports and the entry path

| Setting | Where it comes from |
| --- | --- |
| Port | `http.port`, else the `PORT` [environment variable](https://frontmcp.dev/reference/server/config-files#environment-variables), else `3000`. `PORT` is read when the server's options are parsed, so setting `process.env.PORT` in code before `bootstrap()` works. (Changed in 1.9: before, it was read when `@frontmcp/sdk` was first imported, and a later change had no effect.) |
| Address | `127.0.0.1` unless `http.security.bindAddress` or `FRONTMCP_BIND_ADDRESS` says otherwise. See [`http`](https://frontmcp.dev/reference/sdk/frontmcp#http). |
| MCP endpoint | `http.entryPath`, else the `FRONTMCP_HTTP_ENTRY_PATH` environment variable, else the root, `/`. |
| Health checks | `/healthz`, `/health` and `/readyz`, whatever the entry path. |
| With `splitByApp: true` | Each app at `/<app id>` under the entry path. The root answers 404. |

`frontmcp dev` sets `FRONTMCP_HTTP_ENTRY_PATH` to the path in its own configuration.

### Lifecycle and shutdown

`bootstrap()` goes through these steps:

1. Parses the configuration, and fails on anything invalid.
2. Builds the server: its providers, its logger, then its scopes, which build the apps and their tools, resources, prompts, jobs and plugins. This is `ready`.
3. Starts the HTTP server and waits until it listens.
4. Runs every scope's `onServerStarted()` callbacks, then logs `FrontMCP bootstrap complete`.

On `SIGINT` or `SIGTERM` it shuts the server down: it stops taking new connections, lets requests in flight finish for up to 5 seconds, closes what the server holds (its Redis connections, and a distributed instance's heartbeat), and exits `0`, or `1` when a step fails or the whole shutdown takes more than 10 seconds. See [stopping a Node server](https://frontmcp.dev/reference/deployment/node#stopping). A server on a Unix socket is the exception: it removes the socket file and exits at once, as described above. (Changed in 1.9.2: before, `bootstrap()` installed no signal handlers, and `SIGTERM` ended the process at once.)

Changed in 1.9.7: before, a failed initialization left the scopes it built alive. Now `ready` awaits shutdown before rejecting. This applies to the creation methods above and to `connect*()`. Cleanup failures are logged as `Graceful shutdown: … failed` and never replace the startup error. If cleanup does not settle within 10 seconds, FrontMCP logs `Releasing a server that failed to start did not finish within 10000 ms` and rejects with the startup error; the deadline bounds the wait, not the unfinished cleanup.

Changed in 1.9.7: `shutdown()` called during initialization waits for it to settle before collecting scopes. Before, it could finish with an incomplete scope list and leave scopes alive, on either a successful or a failed start.

#### Caveats

- **Importing a decorated class starts a server.** Keep the configuration in a module of its own, and decorate a class with it only in the entry file, so scripts, tests and workers can import the configuration without starting anything.
- **Only `runStdio()` and `connect()` accept the decorated class.** The other methods need the configuration object. Pass `getDecoratorConfig(Server)` if all you have is the class.
- **With `splitByApp: true`, or a `standalone` app, only `bootstrap()`, `createHandler()` and `createFetchHandler()` serve every app.** `runStdio()` serves the [primary scope](#instance-members) only: the apps that aren't `standalone`, or with `splitByApp: true` the first app. `createDirect()` and `connect()` do too, unless they're given an `app`, which serves that app's own scope. See [Apps, discovery and splitting](https://frontmcp.dev/reference/server/apps). (Changed in 1.9.3: before, `createDirect()` and `connect()` had no `app` option. Changed in 1.9.2: `createFetchHandler()` serves them all; see [its entry paths](https://frontmcp.dev/reference/sdk/create-fetch-handler).)
- Declare one `@FrontMcp` per process. Each `bootstrap()` starts another server, and a second one on the same port fails with `EADDRINUSE`.

---

## Usage

### Starting the server after setup

A server whose tools need a database connection, or secrets fetched at startup, shouldn't accept calls before they're ready. Keep the configuration in its own module, turn off `serve`, and call `bootstrap()` when you're ready:

```ts config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { port: 8080, entryPath: "/mcp" },
};
```

```ts main.ts
import "reflect-metadata";
import { FrontMcp, FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";
import { connectToDatabase } from "./db";

@FrontMcp({ ...config, serve: false })
export default class HelpDeskServer {}

try {
  await connectToDatabase();
  await FrontMcpInstance.bootstrap(config);
} catch (err) {
  console.error("help-desk didn't start:", err);
  process.exit(1);
}
```

Clients connect to `http://localhost:8080/mcp`. This needs a real port, so run it locally.

### Reading a server's configuration

`@FrontMcp` records the configuration on the class, with every default filled in. `getDecoratorConfig()` reads it back, and it's what to pass to the methods that don't take the class. In the Playground, `FRONTMCP_SCHEMA_EXTRACT` is set, so defining `HelpDeskServer` records the configuration and starts nothing; the Playground then serves it with `createFetchHandler()`.

```ts main.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDesk] })
export default class HelpDeskServer {}
```

```ts help-desk.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one support ticket by its id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, title: "Cannot log in", status: "open" };
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [GetTicket] })
export class HelpDesk {}
```

```ts config.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance, getDecoratorConfig } from "@frontmcp/sdk";
import HelpDeskServer from "./main";

test("the class carries its configuration, with defaults", () => {
  const config = getDecoratorConfig(HelpDeskServer);
  expect(config).toMatchObject({
    info: { name: "help-desk", version: "1.0.0" },
    serve: true,
    transport: { sessionMode: "stateful", protocol: "legacy" },
  });
});

test("the methods that don't take the class take its configuration", async () => {
  await expect(FrontMcpInstance.createForGraph(HelpDeskServer as never)).rejects.toThrow("Invalid input: expected object, received undefined");
  const instance = await FrontMcpInstance.createForGraph(getDecoratorConfig(HelpDeskServer)!);
  expect(instance.getConfig().info.name).toBe("help-desk");
});
```

### Inspecting a server without serving it

`createForGraph()` builds the whole server, apps, tools and all, and doesn't serve it. Use it to check what a configuration produces: in a test, or in a script that lists every tool. A server has one scope, `root`, holding every app's tools. With `splitByApp: true` it has one scope per app, each at its own path, and a `standalone` app always gets one of its own.

```ts inspect.test.ts active
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { Billing, BillingApi, HelpDesk } from "./apps";

const info = { name: "help-desk", version: "1.0.0" };

function describe(instance: FrontMcpInstance) {
  return instance.getScopes().map((scope) => ({
    id: scope.id,
    path: scope.routeBase,
    tools: scope.tools.getTools().map((tool) => tool.metadata.name),
  }));
}

test("one scope holds every app's tools", async () => {
  const instance = await FrontMcpInstance.createForGraph({ info, apps: [HelpDesk, Billing] });
  expect(describe(instance)).toEqual([{ id: "root", path: "", tools: ["get_ticket", "refund_invoice"] }]);
});

test("with splitByApp, each app is a scope at its own path", async () => {
  const instance = await FrontMcpInstance.createForGraph({ info, apps: [HelpDesk, Billing], splitByApp: true });
  expect(describe(instance)).toEqual([
    { id: "help-desk", path: "/help-desk", tools: ["get_ticket"] },
    { id: "billing", path: "/billing", tools: ["refund_invoice"] },
  ]);
  expect(instance.getPrimaryScope()?.id).toBe("help-desk");
});

test("a standalone app gets a scope of its own; createDirect() serves the primary one, or that one with `app`", async () => {
  const config = { info, apps: [BillingApi, HelpDesk] };
  const instance = await FrontMcpInstance.createForGraph(config);
  expect(describe(instance)).toEqual([
    { id: "billing-api", path: "/billing-api", tools: ["refund_invoice"] },
    { id: "root", path: "", tools: ["get_ticket"] },
  ]);
  expect(instance.getPrimaryScope()?.id).toBe("root");
  expect(instance.getAppScope("billing-api").routeBase).toBe("/billing-api");
  expect(() => instance.getAppScope("help-desk")).toThrow('No endpoint serves app "help-desk" on its own');

  const server = await FrontMcpInstance.createDirect(config);
  const { tools } = await server.listTools();
  await server.dispose();
  expect(tools.map((tool) => tool.name)).toEqual(["get_ticket"]);

  const billing = await FrontMcpInstance.createDirect(config, { app: "billing-api" });
  const billingTools = await billing.listTools();
  await billing.dispose();
  expect(billingTools.tools.map((tool) => tool.name)).toEqual(["refund_invoice"]);
});
```

```ts apps.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one support ticket by its id", inputSchema: { id: z.string() } })
class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, status: "open" };
  }
}

@Tool({ name: "refund_invoice", description: "Refund an invoice", inputSchema: { invoice: z.string() } })
class RefundInvoice extends ToolContext {
  async execute({ invoice }: { invoice: string }) {
    return { invoice, refunded: true };
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [GetTicket] })
export class HelpDesk {}

@App({ id: "billing", name: "Billing", tools: [RefundInvoice] })
export class Billing {}

@App({ id: "billing-api", name: "Billing API", tools: [RefundInvoice], standalone: true })
export class BillingApi {}
```

A scope's registries are described in [Scope and registries](https://frontmcp.dev/reference/sdk/scope).

### Serving over stdio

Local clients like Claude Desktop and Cursor can start your server as a subprocess and talk to it over stdin and stdout. Give them an entry file that calls `runStdio()` with the configuration, and never imports the decorated class:

```ts stdio.ts
import "reflect-metadata";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

FrontMcpInstance.runStdio(config);
```

```json claude_desktop_config.json
{
  "mcpServers": {
    "help-desk": { "command": "node", "args": ["/abs/path/dist/stdio.js"] }
  }
}
```

Or keep one entry file and set `FRONTMCP_STDIO=1` in the client's configuration (`"env": { "FRONTMCP_STDIO": "1" }`): `@FrontMcp` then calls `runStdio()` instead of starting an HTTP server. Anything the server writes to stdout that isn't a protocol message breaks the client, so log to stderr.

---

## Troubleshooting

### `Invalid input: expected object, received undefined`

The error is a `ZodError` with `invalid_union`, whose issues are at `info` and at `apps` (`expected array, received undefined`). You passed the decorated class to a method that takes the configuration object: `bootstrap()`, `createFetchHandler()`, `createDirect()` or `createForGraph()`. The class has no `info` or `apps` of its own: they're recorded on it. (Changed in 1.9: before, the issue was `expected object, received function`.)

```ts
// 🚩 The class
await FrontMcpInstance.createDirect(HelpDeskServer);

// ✅ Its configuration
await FrontMcpInstance.createDirect(getDecoratorConfig(HelpDeskServer)!);
```

Better, export the configuration from a module of its own and pass that. [The first Playground](#reading-a-servers-configuration) shows both.

### `listen EADDRINUSE: address already in use 127.0.0.1:3000`

Something already listens on the port: another copy of your server, or a second `bootstrap()` in the same process, often because a script imported the decorated class and then called `bootstrap()` too. Stop the other process, or choose another port with `http.port` or the `PORT` environment variable. Set `PORT` in the environment that starts the process, or in code before the server starts.

### A script that imports my server starts listening

Importing a class decorated with `@FrontMcp` starts a server. Import the configuration module instead, or set `serve: false` on the decorator and start the server where you want it with `bootstrap()`.

### `Serverless handler is not initialized`

`getServerlessHandlerAsync()` found no handler, because `FRONTMCP_SERVERLESS` wasn't set to `1` or `true` when the decorated class was defined. Without it, `@FrontMcp` called `bootstrap()` and started an HTTP server instead. Set the variable in the platform's environment, or skip the decorator and call `createHandler(config)` or [`createFetchHandler(config)`](https://frontmcp.dev/reference/sdk/create-fetch-handler) yourself.

### My client gets `404` or `Cannot POST /mcp`

The MCP endpoint is at the root unless `http.entryPath` is set. See [the `@FrontMcp` entry](https://frontmcp.dev/reference/sdk/frontmcp#my-client-gets-404-or-cannot-post-mcp).

### Only my first app is reachable

The server uses `splitByApp: true` and is served by `runStdio()`, or by `createDirect()` or `connect()` without an `app`, which use the first scope only. Pass `{ app }` to `createDirect()` or `connect()` for another app's scope, serve the server with `bootstrap()`, `createHandler()` or `createFetchHandler()`, which give each app its path, or turn `splitByApp` off.

### `Releasing a server that failed to start did not finish within 10000 ms`

Initialization failed, and cleanup is still pending after its 10-second deadline. `ready` rejects with the original startup error, so fix that error first. Check provider disposal, disconnect hooks and storage shutdown for work that never settles. The deadline stops waiting; it does not cancel the unfinished cleanup. New in 1.9.7.
