Skip to content

Performance patterns

Keep Concourse pages fast, responsive, and safe to cache

Concourse uses Next.js Cache Components and partial prefetching. New code must make a clear choice about freshness, streaming, and feedback before it adds a read or action.

Choose a cache boundary

Cache the smallest function that owns stable data. Keep authentication and access checks outside the cached function. Organization membership, SSO, and administration reads remain request-bound.

Use use cache for data that is safe to share between users, such as processed documentation. Pass serializable arguments and set cacheLife in the same function. Add a tag when a write can make the result stale.

After a successful mutation, call updateTag for the changed data. Call revalidatePath when the rendered route also needs to refresh.

Use use cache: private only when a request API must be read inside a cache boundary and the value must stay in browser memory. It is suitable for the docs sidebar cookie. It is not a server cache for user data.

Use use cache: remote only for an expensive, shared, low-cardinality operation where the team has approved remote cache storage, cost, and invalidation. A remote cache is not a default replacement for the in-memory cache.

Authentication, authorization, membership, SSO, account security, and audit reads stay fresh and request-bound. Do not place a session, headers(), or access decision inside a shared use cache scope.

See the Next.js use cache reference, cacheLife, and cacheTag for the framework rules.

Use the Better Auth session cache

Better Auth uses a separate signed cookie cache for short-lived session data. Concourse keeps the compact cache enabled for five minutes, as configured in packages/auth/lib/identity/configuration.ts. This reduces repeated session database reads for the application shell's readable navigation display.

The cookie cache is not a content cache. Use Next.js use cache for shareable documentation content, with an explicit lifetime and invalidation tag. Keep session and access decisions outside that content cache.

For a static documentation site, keep the shared layout free of cookies(), headers(), and other request-time reads. A request-time read in a layout can block the route before its page fallback is shown and can prevent full route prefetching. Store browser-only preferences in client state or an isolated boundary when the preference is not part of the page content. Do not add a route loading.tsx to generated content unless the route becomes dynamic. A loading boundary changes the prefetch shape, and build-time content does not need one.

When partialPrefetching is enabled, use prefetch={true} for links to static pages whose content depends on params or searchParams. The default partial prefetch only prepares the shared shell; the explicit value resolves the URL-specific page before the click. Use this for a small static docs navigation, not for large lists of links or expensive dynamic pages.

Use the cookie cache for:

  • application navigation chrome
  • readable session display that does not decide access

Use disableCookieCache: true for:

  • account security
  • user management
  • organization membership and SSO management
  • system administration
  • every authorization check or protected action

The application shell can briefly show stale navigation chrome. Protected queries, route handlers, Server Actions, and commands must use authoritative access checks. The proxy checks only for a cookie and redirects missing cookies. It does not decide access.

React cache belongs to the application's render adapter. Backend packages receive no memoizer. The app can reuse a completed read within one render. Each backend operation checks its own permissions and required freshness. Never retain authorization results across requests or reuse them for commands. ADR 0002 records render-local reuse and per-request access checks.

Server-render reads disable renewal with disableRefresh: true. The signed-in shell mounts Better Auth's session hook, which renews cookies through the existing handler on mount, focus, reconnect, and authentication events. No interval polling or navigation-triggered renewal is configured. The database-backed deployment does not use Better Auth's stateless refreshCache option.

A concurrent revocation takes effect on the next request. Streamed page errors need not preserve literal HTTP 403 or 404 statuses, but must show the correct denial screen without protected data. API status contracts remain unchanged.

Read the Better Auth performance guide and options reference before changing this split.

Stream slow work

Put a Suspense boundary around a slow database or network section. The fallback must match the loaded shape and set aria-busy="true" with a screen-reader-only loading label. Add a route-level loading.tsx when the page can wait before it renders.

Prefer a small number of meaningful boundaries. Keep access checks outside the boundary when the check controls whether the page exists.

Show action feedback

Every action that can wait for a Server Action, fetch, or navigation needs immediate feedback:

  • Use useFormStatus in a submit button inside a Server Action form.
  • Use local pending state for client fetches and native forms.
  • Show a Spinner and a useful label such as Saving…, Deleting…, or Opening….
  • Disable only the submitted control or row when that does not cancel browser navigation. Native navigation forms stay submit-capable and set aria-busy on the form.
  • Restore the control and show an error when a client request fails.

Buttons that only open a local menu, disclosure, or copy already-loaded text do not need a network pending state.

Show navigation feedback

Use a route loading.tsx or a nested Suspense skeleton for page transitions. Internal links also use the shared pending-link pattern. It reserves a fixed-size hint and reveals it after a short delay with useLinkStatus, so fast navigations stay quiet and slow navigations remain clear.

Use useTransition around client-side router.push calls and show a local status near the initiating control. Keep protected-route prefetch rules intact. Test the slow path in a browser when you change these contracts.

How this is checked

RuleChecked by
A use cache function does not read a request APIThe Next.js compiler rejects the read
Cache boundaries, invalidation tags, and feedback on new codeCode review, against this page
The slow path shows its pending and loading statesA browser check when you change one of these contracts