惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
罗磊的独立博客
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
S
Schneier on Security
V
Vulnerabilities – Threatpost
T
Tenable Blog
博客园 - 【当耐特】
博客园 - 聂微东
I
Intezer
S
Securelist
C
Cisco Blogs
腾讯CDC
T
The Exploit Database - CXSecurity.com
NISL@THU
NISL@THU
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
Security Archives - TechRepublic
Security Archives - TechRepublic
D
Darknet – Hacking Tools, Hacker News & Cyber Security
美团技术团队
P
Proofpoint News Feed
M
MIT News - Artificial intelligence
AWS News Blog
AWS News Blog
A
About on SuperTechFans
Last Week in AI
Last Week in AI
Cloudbric
Cloudbric
S
Secure Thoughts
PCI Perspectives
PCI Perspectives
The Last Watchdog
The Last Watchdog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
C
CERT Recently Published Vulnerability Notes
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
The Register - Security
The Register - Security
博客园 - 叶小钗
博客园 - 司徒正美
博客园_首页
Google DeepMind News
Google DeepMind News
Hacker News: Ask HN
Hacker News: Ask HN
Spread Privacy
Spread Privacy
雷峰网
雷峰网
I
InfoQ
The Hacker News
The Hacker News
T
Threat Research - Cisco Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Schneier on Security
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Prisma Server Actions in Next.js 16: the patterns that work and the N+1 that sneaks up on you
Juan Torchia · 2026-05-18 · via DEV Community

Prisma Server Actions in Next.js 16: the patterns that work and the N+1 that sneaks up on you

Next.js 16 shipped recently with App Router improvements and Server Actions stabilized as a first-class primitive. The community is adopting Server Actions as the natural replacement for API routes on mutations. The migration looks obvious — less boilerplate, co-location with the component, shared types between client and server. I started moving in that direction too. And somewhere along the way I ran into an N+1 that didn't come from Prisma: it came from how I was composing the Actions.

My thesis is this: Prisma ORM 5 doesn't introduce N+1 in Server Actions. Action composition does — the pattern of calling multiple independent Actions from the same component, or chaining them without collapsing the queries. It's an architecture problem, not an ORM problem. And it has a solution, but you have to know where to look.


Classic N+1 vs. composition N+1 in Server Actions

The classic N+1 with Prisma is well-known: you iterate over a list and fire a separate query for each item because you forgot the include. The official Prisma docs on query optimization cover it precisely — use include or select with nested relations, or for more complex cases, findMany with relational filters instead of queries in a loop.

The composition N+1 in Server Actions is different. It doesn't show up inside the body of a single Action — it shows up when the component calls multiple Actions in sequence or in parallel, and each Action opens its own connection with its own Prisma cursor. Under SSR load, that becomes connection pool pressure that never appears in local tests.

Look at this problematic pattern:

// app/dashboard/page.tsx
// ⚠️ Problematic pattern: three independent Actions
// each one opens its own connection to the pool

import { getUserProfile } from "@/actions/user"
import { getRecentOrders } from "@/actions/orders"
import { getNotifications } from "@/actions/notifications"

export default async function DashboardPage() {
  // Three separate round-trips, three pool connections
  const profile = await getUserProfile()
  const orders = await getRecentOrders()
  const notifications = await getNotifications()

  return <Dashboard profile={profile} orders={orders} notifications={notifications} />
}

Enter fullscreen mode Exit fullscreen mode

Each of those Actions has its own prisma.user.findUnique, its own prisma.order.findMany, its own prisma.notification.findMany. Three queries that could be resolved with a single well-designed call — or at minimum with Promise.all to parallelize them.


The connection pool under SSR load

Prisma uses an internal connection pool. In Next.js App Router with SSR, each request can fire multiple Server Actions in the same render. If every component on the page calls its own Action, the pool receives a short but intense burst of connections per user visit.

The most common pattern that generates this problem is using prisma as a global singleton alongside PrismaClient instantiated in each separate module. Prisma's documentation explicitly recommends using a singleton instance in serverless and SSR environments:

// lib/prisma.ts
// Singleton pattern recommended by Prisma for Next.js
// Source: https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance

import { PrismaClient } from "@prisma/client"

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "warn", "error"] : ["error"],
  })

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

Enter fullscreen mode Exit fullscreen mode

If you skip this pattern, every hot reload in development — and potentially every cold start in production with some providers — can instantiate a fresh PrismaClient with its own pool. The result: exhausted connections with no obvious warning in the logs.


The patterns that work: collapsing queries into a single Action

The antidote to the composition N+1 is simple to state but requires discipline: one Action per use case, not one Action per entity. Instead of three independent Actions for the dashboard, one single Action that groups the three queries with Promise.all:

// actions/dashboard.ts
// ✅ Correct pattern: one Action that collapses the queries
// Promise.all for real parallelism within the same connection

"use server"

import { prisma } from "@/lib/prisma"
import { auth } from "@/lib/auth"

export async function getDashboardData() {
  const session = await auth()
  if (!session?.user?.id) throw new Error("Not authenticated")

  const userId = session.user.id

  // Single pool invocation — three queries in parallel
  const [profile, orders, notifications] = await Promise.all([
    prisma.user.findUnique({
      where: { id: userId },
      select: { name: true, email: true, avatarUrl: true },
    }),
    prisma.order.findMany({
      where: { userId, createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } },
      orderBy: { createdAt: "desc" },
      take: 10,
    }),
    prisma.notification.findMany({
      where: { userId, read: false },
      orderBy: { createdAt: "desc" },
      take: 5,
    }),
  ])

  return { profile, orders, notifications }
}

Enter fullscreen mode Exit fullscreen mode

The difference isn't just about queries — it's about design. An Action that groups data for a specific use case is easier to cache, easier to test, and more honest about what problem it's actually solving.


The forgotten include and the query that multiplied

The classic N+1 still lives inside Actions. If you iterate over results and fire a nested query per item, Prisma isn't going to save you — that's on you. The most frequent pattern I see in codebases just starting with Server Actions:

// ⚠️ Classic N+1 inside an Action
// One query per order to fetch the product

"use server"

import { prisma } from "@/lib/prisma"

export async function getOrdersWithProducts(userId: string) {
  const orders = await prisma.order.findMany({ where: { userId } })

  // ❌ N+1: one query per order
  const ordersWithProduct = await Promise.all(
    orders.map(async (order) => {
      const product = await prisma.product.findUnique({
        where: { id: order.productId },
      })
      return { ...order, product }
    })
  )

  return ordersWithProduct
}

Enter fullscreen mode Exit fullscreen mode

The correct fix is to collapse with include:

// ✅ Correct include: a single query with implicit JOIN
// Prisma collapses everything into a single round-trip

"use server"

import { prisma } from "@/lib/prisma"

export async function getOrdersWithProducts(userId: string) {
  return prisma.order.findMany({
    where: { userId },
    include: {
      product: {
        select: { name: true, price: true, imageUrl: true },
      },
    },
    orderBy: { createdAt: "desc" },
    take: 20,
  })
}

Enter fullscreen mode Exit fullscreen mode

The select inside the include matters: you're not pulling the full product object, you're pulling exactly the fields the component needs. That reduces the serialized payload Next.js has to transfer between server and client.


Real gotchas: what the 15-minute tutorial doesn't cover

"use server" doesn't guarantee automatic serialization of Prisma errors. If an Action throws a PrismaClientKnownRequestError (say, a constraint violation), that error doesn't reach the client the way you'd expect in all cases. You need to wrap with try/catch and serialize the error explicitly:

// actions/user.ts
// Explicit Prisma error handling in Server Actions

"use server"

import { prisma } from "@/lib/prisma"
import { Prisma } from "@prisma/client"

export async function createUser(data: { email: string; name: string }) {
  try {
    return await prisma.user.create({ data })
  } catch (error) {
    // Unique constraint violation (P2002 in Prisma)
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === "P2002") {
        return { error: "That email is already registered" }
      }
    }
    // Unexpected error: log it, don't expose it
    console.error("[createUser]", error)
    return { error: "Internal error. Please try again." }
  }
}

Enter fullscreen mode Exit fullscreen mode

Query logging in development is your best diagnostic tool. The singleton above already includes log: ["query"] in development — that lets you see exactly how many queries each render fires. If you see the same SELECT repeated N times in the terminal, you have an N+1 and you can attack it before it hits production.

Server Actions and React 19 useOptimistic can mask the problem. If you use useOptimistic to update the UI before the Action resolves, perceived latency drops — but the queries are still there. Don't confuse improved UX with optimized queries.

This connects to something I already documented when looking at how OpenTelemetry in Spring Boot reveals the real problem when the log says OK: observability surface matters. In Next.js 16, if you don't have query traces, the Action log can look healthy while queries multiply underneath.


FAQ: Prisma Server Actions Next.js 16 N+1

Why does N+1 appear in Server Actions when it didn't in my API routes?
In API routes, the natural pattern was one route = one handler = one query. In Server Actions, co-location with the component invites you to create one Action per entity, and components end up calling several Actions in the same render. That composition generates multiple round-trips that never existed in an API route because the query was centralized.

Does Prisma ORM 5 have any mechanism to automatically detect N+1?
Not automatically at runtime, but you can enable query logging (log: ["query"]) to see them in development. There are community proposals for a native N+1 detector, but as of this post it's not a stable feature. The official optimization docs document the patterns to avoid, but detection is still manual or via external tooling.

How many PrismaClient instances should I have in a Next.js 16 project?
One. Using the singleton pattern with globalThis. More than one instance means more than one connection pool, which under SSR load can exhaust available database connections. This is especially critical on serverless providers where each function can have its own process.

Is Promise.all inside an Action enough to fix the pool problem?
For multiple independent queries inside a single Action, yes: Promise.all parallelizes them within the same invocation and the pool handles a single connection (or the minimum needed). What Promise.all does not fix is when you have multiple independent Actions fired from different components in the same render — that needs consolidation at the architecture level.

How does this affect Next.js 16 caching?
Next.js 16 has Data Cache and Full Route Cache. If you use fetch or unstable_cache, you can cache the result of a Server Action. But the N+1 happens before the cache — if the Action isn't cached (mutations, data with no-store), every request executes the queries. The right pattern is to cache the entire Action with unstable_cache when the data allows it, not to cache individual queries inside it.

Does this pattern also apply to Prisma with pure Server Components (no Actions)?
Yes, but with a difference: in Server Components without Actions, queries live directly in the component and Next.js can do component-level caching more easily. The composition problem is more acute with Server Actions because the mental model of "one Action = one button or form" leads to excessive granularity that multiplies round-trips.


What I'm keeping and what I'm not buying

I'm keeping this pattern: one Action per use case, not one Action per entity. That's the most important mindset shift when migrating from API routes to Server Actions with Prisma.

What I'm not buying is the narrative that Server Actions automatically simplify the data model. They simplify the boilerplate — shared types, no explicit endpoint — but the responsibility to not multiply queries is still yours. If you were coming from API routes where one route = one well-considered query, jumping to Actions can lead to query sprawl that's actually worse.

The honest trade-off: Server Actions win on DX and co-location. They lose on visibility into which queries fire per render if you don't have logging active. Before deploying any page with multiple Actions, pull up the dev terminal with log: ["query"] running and count how many SELECTs appear per render. If the number surprises you, you have work to do.

This connects directly to what I documented in Prisma vs JDBC: the benchmark that almost made me blame the wrong ORM — the ORM is rarely the problem. Query shape is. And in Next.js 16 with Server Actions, shape is defined by the Action architecture, not by Prisma.

For those coming from the Spring Boot world, there's an interesting parallel with retry budget and amplification: every abstraction that looks like a simplification introduces its own amplification vector. In Server Actions, that vector is granular query composition.


Sources:


This article was originally published on juanchi.dev