Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Why Your Next.js SaaS Gets Slow as It Grows -- Caching Without Redis

September 23, 2026
nextjssaasneon-dbdrizzle-ormperformance

Most Next.js SaaS founders ship their first feature, watch it work fast, and move on. Then at some point -- maybe 500 users, maybe 50 -- dashboard pages start taking 800ms. You add a few Drizzle relations, the query count triples. Now it is 1.5 seconds per load.

The standard answer is Redis. But Redis on Upstash or Railway adds $20-50 per month, another service to monitor, and cache invalidation bugs that are subtle and hard to debug. For most SaaS products at the early growth stage, you do not need it.

Here is the caching strategy that covers the most common bottlenecks in a Next.js SaaS without adding a single external service.

The Three Places Your SaaS Gets Slow

Before you cache anything, know where the time actually goes:

  1. Database queries -- a dashboard that runs 12 separate Drizzle queries on every page load
  2. Server component waterfalls -- components that fetch data one after another instead of at the same time
  3. Repeated computation -- aggregates like "total revenue this month" or "active user count" recalculated on every request

Each one has a different fix. Caching is only one of them.

Layer 1: Parallelize Your Server Component Fetches

Before you add any caching, check whether your server components are waiting for each query to finish before starting the next one. This is the most common cause of slow pages and the easiest fix.

// Slow -- each query waits for the previous one
const user = await getUser(userId)
const posts = await getPostsByUser(userId)
const stats = await getStats(userId)
// Fast -- all three run at the same time
const [user, posts, stats] = await Promise.all([
  getUser(userId),
  getPostsByUser(userId),
  getStats(userId),
])

If your page makes three independent database calls, parallelizing them can cut load time by two thirds with no infrastructure change at all.

Layer 2: Next.js unstable_cache for Expensive Queries

Most Drizzle queries bypass the built-in Next.js fetch cache because they use db.select() directly instead of fetch(). The fix is unstable_cache, which wraps any async function in a server-side cache with a TTL you control:

import { unstable_cache } from 'next/cache'
 
export const getDashboardStats = unstable_cache(
  async (userId: string) => {
    return db.select({ count: sql<number>`count(*)` })
      .from(postTable)
      .where(eq(postTable.authorId, userId))
  },
  ['dashboard-stats'],  // cache key prefix
  { revalidate: 60 }    // seconds until stale
)

The first request is slow. Every request after is instant until the TTL expires or you call revalidateTag('dashboard-stats') on a write.

Use it for: aggregates, counts, dashboard summaries, anything expensive that does not change on every user action.

Skip it for: user-specific data that must always be fresh -- payment status, settings, unread notifications.

Layer 3: Fix Your Database Connection Strategy on Vercel

Serverless functions on Vercel open a new database connection on every invocation. With 20 concurrent users, that is 20 simultaneous connection attempts to Neon. Cold connections add 50-200ms per request, and they compound on every page that runs multiple queries.

Neon ships a built-in connection pooler at a separate connection string (the URL ends in -pooler.neon.tech). Switching your DATABASE_URL to the pooled endpoint is a one-line environment variable change that removes cold-connection overhead across your entire app. The deployment guide at /docs/deployment walks through this alongside the environment variable setup on Vercel.

If you want to go deeper on the Vercel deployment flow itself, the post on deploying your Next.js SaaS to Vercel covers migrations and environment variables in detail.

A Decision Framework Before You Reach for Redis

When a page is slow, work through this in order:

  • Are your data fetches running sequentially? Parallelize with Promise.all first.
  • Is the slow query an aggregate or count that changes every few minutes? Wrap it in unstable_cache with a short TTL.
  • Is the slow query static reference data (plan limits, feature flags)? Cache for 5 minutes or longer.
  • Do you need cache invalidation triggered by a specific user write, not a timer? Now you need Redis -- or revalidateTag if the data lives in a Next.js route.

Most SaaS products never need Redis until they are generating meaningful revenue. The stack in the Next.js SaaS Boilerplate -- Neon DB with pooling, Drizzle ORM, and Next.js server components -- gives you enough headroom to grow without it.

What Actually Needs Redis Later

When you do hit the ceiling, the signals are specific:

  • You need to invalidate a cached value the moment a user writes, not 60 seconds later
  • You are storing sessions across multiple Vercel regions and they need to stay in sync
  • You need a job queue that survives a function timeout

None of those are day-one problems. When you reach them, the module architecture in the boilerplate makes adding a Redis layer to a single service straightforward -- you do not have to touch your routes or UI components.

Start With the Bottleneck You Can See

Pick the slowest page in your app. Open your browser's network tab and look at the time-to-first-byte. If it is over 500ms, you almost certainly have either sequential queries you can parallelize or an aggregate you can cache for 30 seconds.

Fix that first. For most early-stage SaaS products, those two changes get you to sub-200ms on pages that were taking over a second.

The Next.js SaaS Boilerplate is built so you can make both changes without refactoring your data layer -- get it and ship faster.