diff --git a/docs/framework/solid/guides/ssr.md b/docs/framework/solid/guides/ssr.md
index f8f8eb43c07..8d842a0ee42 100644
--- a/docs/framework/solid/guides/ssr.md
+++ b/docs/framework/solid/guides/ssr.md
@@ -3,4 +3,195 @@ id: ssr
title: SSR
---
-Will come soon
+This guide covers using Solid Query with server rendering, specifically with [SolidStart](https://start.solidjs.com/).
+
+If you haven't already, it's worth reading the [Prefetching & Router Integration](./prefetching.md) guide first.
+
+## Solid Query's hydration model
+
+Solid Query's `useQuery` is built on top of Solid's own [`createResource`](https://docs.solidjs.com/reference/basic-reactivity/create-resource), rather than a `dehydrate`/`hydrate` pair you call yourself. That has a consequence for this guide: **there is no `dehydrate`, `hydrate`, or `HydrationBoundary` in your application code.**
+
+Instead, when `useQuery` runs on the server:
+
+1. It resolves the query through the same `QueryClient` cache a client render would use, with `retry` forced off and `throwOnError` forced on — see [Error handling](#error-handling) below.
+2. The resolved value is a plain, serializable snapshot of the query result (functions like `refetch` are stripped, since they can't cross the network).
+3. Solid's own resource streaming carries that snapshot down to the client — the same mechanism `createResource` uses for any async resource, query-related or not.
+4. On the client, once the snapshot arrives, Solid Query hydrates the query cache from it internally, before your component ever sees a loading state for that query.
+
+So the three steps you'd normally implement by hand — prefetch, dehydrate, hydrate — collapse into "call `useQuery` (or prefetch with `queryClient.query`), and let Solid's streaming take care of the rest." The tradeoff is that Solid Query decides what gets serialized and when, rather than you calling `dehydrate` with your own filtering logic.
+
+One consequence worth calling out: a query that resolved on the server, and is read with a `staleTime` (or without `initialData`), does not refetch on mount when it hydrates on the client, since Solid Query already knows it has fresh-enough data from the server render. It behaves the same as any other successful query after that: `staleTime` governs whether it refetches on the next trigger (window focus, a new mount, `invalidateQueries`, and so on).
+
+## Initial setup
+
+As with any server-rendered app, create the `QueryClient` per request rather than at module scope, so cached data from one visitor's request is never shared with another's:
+
+```tsx
+// src/app.tsx
+import { MetaProvider, Title } from '@solidjs/meta'
+import { Router } from '@solidjs/router'
+import { FileRoutes } from '@solidjs/start/router'
+import { Suspense } from 'solid-js'
+import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
+
+export default function App() {
+ // Constructing the QueryClient inside the component (rather than at module
+ // scope) means SolidStart creates a fresh one for every request, so data
+ // is never shared between users.
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ // staleTime governs refetches from later triggers (a new mount, window
+ // focus, and so on) — see Solid Query's hydration model above for why
+ // the very first mount after SSR doesn't need it to skip a refetch.
+ staleTime: 5 * 1000,
+ },
+ },
+ })
+
+ return (
+
+ (
+
+ My App
+ {props.children}
+
+ )}
+ >
+
+
+
+ )
+}
+```
+
+The `` boundary matters: `useQuery` suspends while a query is pending, both on the server (so SolidStart can wait for it, or stream past it) and on the client (during the very first render, before hydration data arrives).
+
+## Using `useQuery` directly
+
+Because hydration is automatic, the simplest way to get server-rendered data is to just call `useQuery` where you need it — no separate prefetch step required:
+
+```tsx
+import { useQuery } from '@tanstack/solid-query'
+import { For, Suspense } from 'solid-js'
+
+function Posts() {
+ const query = useQuery(() => ({
+ queryKey: ['posts'],
+ queryFn: fetchPosts,
+ }))
+
+ return (
+ Loading...}>
+
+ {(post) =>
{post.title}
}
+
+
+ )
+}
+```
+
+On the server, this fetches `posts`, waits for it (because of the surrounding ``), and streams the resolved value down. On the client, that value hydrates the cache before `Posts` ever renders a loading state — as long as the client also renders inside a `` boundary, which the `` in `app.tsx` already provides.
+
+Wrap the same query in Solid's `ErrorBoundary` to handle a failure. On the server, a failed query always throws to the nearest `ErrorBoundary` — see [Error handling](#error-handling) below for why, and for what it takes to get the same behavior on the client too:
+
+```tsx
+import { useQuery } from '@tanstack/solid-query'
+import { ErrorBoundary, For, Suspense } from 'solid-js'
+
+function Posts() {
+ const query = useQuery(() => ({
+ queryKey: ['posts'],
+ queryFn: fetchPosts,
+ }))
+
+ return (
+ (
+
+
Error: {err.message}
+
+
+ )}
+ >
+ Loading...}>
+
+ {(post) =>
{post.title}
}
+
+
+
+ )
+}
+```
+
+## Prefetching with `route.load`
+
+To prefetch a query from a SolidStart route's `load` function, call `queryClient.query(...)`:
+
+```tsx
+import { noop, useQueryClient } from '@tanstack/solid-query'
+
+export const route = {
+ load: () => {
+ const queryClient = useQueryClient()
+ queryClient.query(postsQueryOptions()).catch(noop)
+ },
+}
+
+export default function Posts() {
+ const query = useQuery(() => postsQueryOptions())
+ // ...
+}
+```
+
+`.catch(noop)` keeps a failed prefetch from aborting routing — whatever reads the query afterwards (`useQuery`) will surface the failure on its own. See [Error handling](#error-handling) below for how that failure surfaces differently on the server and the client.
+
+Sharing the query options object (`postsQueryOptions()` above) between `route.load` and the component that reads the data, rather than repeating the `queryKey`/`queryFn` in both places, keeps them from drifting apart — see [`queryOptions`](./query-options.md).
+
+## Streaming with `deferStream`
+
+By default, a query streams to the client as soon as it resolves on the server. Set `deferStream: true` to hold the server's response until that query resolves instead — useful for data that has to be present in the very first response, such as SEO meta tags:
+
+```tsx
+const query = useQuery(() => ({
+ queryKey: ['user'],
+ queryFn: fetchUser,
+ deferStream: true,
+}))
+```
+
+`deferStream` only affects when the _server_ flushes that query's chunk to the client. Setting it on every query on a page makes the server wait for all of them before sending anything.
+
+## Error handling
+
+On the server, `useQuery` always throws when a query fails — regardless of the `throwOnError` option — so it's caught by the nearest `` rather than surfacing as `query.isError`. This is different from the client, where a failed query reports `isError`/`error` unless you opt in to throwing with `throwOnError: true`. Retries are also disabled on the server, so a query throws on its first failure rather than retrying first.
+
+If a query is read with `useQuery` on both the server and the client, set `throwOnError: true` on it explicitly so the client matches what the server already does — otherwise the same failure throws during SSR but only sets `isError` once it's running on the client:
+
+```tsx
+const query = useQuery(() => ({
+ queryKey: ['user'],
+ queryFn: fetchUser,
+ throwOnError: true,
+}))
+```
+
+A `route.load` prefetch isn't affected by this server-only override — `queryClient.query(...)` doesn't take a `throwOnError` option at all, so it simply rejects when the query fails, the same on the server and the client. Catch it (or let it propagate) however fits the route; see [Prefetching with `route.load`](#prefetching-with-routeload) above for the `.catch(noop)` pattern used when a failed prefetch shouldn't abort routing.
+
+## Tips, Tricks and Caveats
+
+### Staleness is measured from when the query was fetched on the server
+
+A query's staleness is based on `dataUpdatedAt`, which is set to when it resolved on the server, not when the client hydrates it. Because `staleTime` defaults to `0`, an SSR'd query without an explicit `staleTime` is treated as stale as soon as its next trigger (window focus, a new mount past the first one, `invalidateQueries`, and so on) checks it, and may refetch in the background then — the first mount on the client is still exempt, per [Solid Query's hydration model](#solid-querys-hydration-model) above. Setting a `staleTime` above `0` (as in the Initial setup example above) avoids this if you don't want it.
+
+### High memory consumption on server
+
+Because the `QueryClient` is created fresh for every request, its cache is held in memory for the duration of that request. Under high request volume this can add up. `gcTime` defaults to `Infinity` on the server, so unless you configure it lower, memory for a request's cache isn't reclaimed automatically — in most cases this is fine, since the whole `QueryClient` instance becomes eligible for garbage collection once the request finishes and nothing references it anymore.