Next.js connection api — Keep Pages Static While Fetching Dynamic Data
When you have a Next.js page that is mostly static content but needs one small piece of live data — the connection api allows you to have both.
The Problem
I introduced commenting on this blog. This meant adding a comment list and form on the blog post page and a comment count in the post cards. The list of blog posts and blog post page are purely static which means that Next.js renders them at build time.
On the other hand, the comments and comment counts are fetched from the database. In CI, when building the app for deployment, there is no database to fetch the comment list and comment count hence the build was failing with ECONNREFUSED. When I asked Cursor for a solution, its suggestion was to wrap the methods in try/catch blocks that return default values
While that works, it would also silently swallow real errors in production too, which isn't great. I knew about dynamic APIs but only the common ones, i.e., cookies, headers, searchParams and fetch and I wasn't using any of them. The connection api, one of the ones I didn't know about was the hero that I needed.
Enter connection()
Next.js exports a connection() function from next/server. When you call it inside a Server Component, you are telling Next.js:
"This component needs a live request. Don't try to prerender it at build time."
import { connection } from "next/server";
async function CommentsLoader({ postId }: { postId: string }) {
await connection(); // ← opts into dynamic rendering, everything below it will be skipped during pre-rendering.
const comments = await getComments(postId);
return <CommentList comments={comments} />;
}At build time, Next.js sees the connection() call and skips the component entirely, rendering the <Suspense> fallback instead. At request time, it runs normally and hits the database.
How I Use It in the Blog Posts List Cards
On list pages (home, /coding, /general, etc.) — each post card renders a tiny <CommentCount> component that calls connection() and fetches just the count for that post. It's also wrapped in <Suspense fallback={null}>, so the cards render instantly with the count popping in after.
// src/components/comments/comment-count.tsx
import { connection } from "next/server";
import { getCommentCount } from "@/lib/db/comments";
export default async function CommentCount({ postId }: { postId: string }) {
await connection();
// Everything below will be excluded from prerendering
const count = await getCommentCount(postId);
if (count === 0) return null;
return <span>{count} comments</span>;
}// Inside PostCard (a regular server component)
<Suspense fallback={<Loader />}>
<CommentCount postId={post.frontmatter.id} />
</Suspense>The key insight: the list pages stay fully static. Only the small <CommentCount> islands become dynamic. The page shell, MDX content, tags, dates — all prerendered at build time. Zero database calls during next build.
The Mental Model
Think of connection() as a boundary marker:
- Above it (the page shell): statically generated, cached, fast
- Below it (the component and its children): rendered per-request, can talk to databases, APIs, etc.
- Suspense: the glue that lets both coexist on the same page
You can always reach for it when a component doesn't use other Dynamic APIs, but you want it to be dynamically rendered at runtime and not statically rendered at build time.
Another scenario where connection() shines is when using APIs that produce different results each time they execute e.g Math.random(), Date.now(), crypto.randomUUID() etc.
async function ChangingContent() {
// Explicitly defer to request/run time
await connection();
// Non-deterministic operations
const random = Math.random();
const date = new Date();
const uuid = crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(16));
const now = Date.now();
return (
<div>
<p>{random}</p>
<p>{now}</p>
<p>{date.getTime()}</p>
<p>{uuid}</p>
<p>{bytes}</p>
</div>
);
}// and you always have to wrap that component in suspense to use it on a page since it defers rendering to request time
<Suspense fallback={<p>Loading..</p>}>
<ChangingContent />
</Suspense>Related Posts (3)
How to fix Tailwind dark mode colors when prefers-color-scheme conflicts with manual theme toggling.
Git Rebase --onto
1 min readLearned about git rebase --onto for moving branches to a different base.