Bright and colorful JavaScript code displayed on a computer screen, showcasing programming.

Payload CMS vs Sanity for Next.js 15: A Technical Comparison

· 6 min read

Next.js 15's enhanced server components and streaming capabilities have changed how developers evaluate CMS options. Payload and Sanity represent two distinct architectural approaches—self-hosted with direct database access versus API-first with managed infrastructure. This comparison examines their performance characteristics, developer experience, and practical implementation differences when building production Next.js applications.

Architecture and Data Flow

The fundamental difference between these systems shapes everything downstream. Payload operates as a Node.js application with direct database connections, while Sanity functions as a hosted API with a content lake architecture.

Payload's approach means the CMS runs alongside the Next.js application, sharing the same runtime environment. This enables direct database queries from server components without additional network hops:

1// app/posts/page.tsx - Direct database access with Payload
2import { getPayloadHMR } from '@payloadcms/next/utilities'
3import configPromise from '@payload-config'
4
5export default async function PostsPage() {
6  const payload = await getPayloadHMR({ config: configPromise })
7  
8  const posts = await payload.find({
9    collection: 'posts',
10    where: {
11      status: { equals: 'published' }
12    },
13    limit: 20,
14    sort: '-publishedAt'
15  })
16
17  return (
18    <div>
19      {posts.docs.map(post => (
20        <article key={post.id}>
21          <h2>{post.title}</h2>
22          <p>{post.excerpt}</p>
23        </article>
24      ))}
25    </div>
26  )
27}

Sanity requires HTTP requests to their Content Lake, introducing network latency but providing global CDN distribution for content delivery:

1// app/posts/page.tsx - API-based access with Sanity
2import { createClient } from '@sanity/client'
3
4const client = createClient({
5  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
6  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
7  apiVersion: '2024-01-01',
8  useCdn: true, // CDN for published content
9})
10
11export default async function PostsPage() {
12  const posts = await client.fetch(`
13    *[_type == "post" && status == "published"] | order(publishedAt desc) [0...20] {
14      _id,
15      title,
16      excerpt,
17      publishedAt
18    }
19  `)
20
21  return (
22    <div>
23      {posts.map(post => (
24        <article key={post._id}>
25          <h2>{post.title}</h2>
26          <p>{post.excerpt}</p>
27        </article>
28      ))}
29    </div>
30  )
31}

Performance Characteristics

Response times differ significantly based on deployment architecture. Payload's direct database access typically delivers sub-10ms query times when the Next.js app and database share infrastructure. Sanity's API requests add 50-150ms of latency depending on geographic distribution, though their CDN can serve cached content faster for read-heavy workloads.

Payload Performance Profile

Direct PostgreSQL or MongoDB queries eliminate HTTP overhead. A typical content query executes in 3-8ms on co-located infrastructure:

1// payload.config.ts - Optimized database configuration
2import { buildConfig } from 'payload/config'
3import { postgresAdapter } from '@payloadcms/db-postgres'
4
5export default buildConfig({
6  db: postgresAdapter({
7    pool: {
8      max: 20,
9      idleTimeoutMillis: 30000,
10      connectionTimeoutMillis: 2000,
11    },
12    // Connection pooling reduces overhead for frequent queries
13  }),
14  collections: [
15    {
16      slug: 'posts',
17      fields: [
18        {
19          name: 'title',
20          type: 'text',
21          required: true,
22        },
23      ],
24      // Database indexes for common queries
25      indexes: [
26        {
27          fields: { status: 1, publishedAt: -1 }
28        }
29      ]
30    }
31  ]
32})

The trade-off: scaling requires managing database connections and read replicas. Applications exceeding 100 requests/second need connection pooling strategies and potentially read replicas for query distribution.

Sanity Performance Profile

Sanity's Content Lake provides global distribution with edge caching. Published content serves from CDN edges with 20-50ms response times globally. Draft content requires origin requests, adding 100-200ms latency:

1// lib/sanity.ts - Optimized Sanity client configuration
2import { createClient } from '@sanity/client'
3
4export const client = createClient({
5  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
6  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
7  apiVersion: '2024-01-01',
8  useCdn: true, // Enable CDN for published content
9  perspective: 'published', // Only fetch published documents
10  stega: {
11    enabled: process.env.NODE_ENV === 'development',
12    studioUrl: '/studio',
13  }
14})
15
16// Separate client for draft content (slower, no CDN)
17export const previewClient = createClient({
18  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
19  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
20  apiVersion: '2024-01-01',
21  useCdn: false, // No CDN for draft content
22  perspective: 'previewDrafts',
23  token: process.env.SANITY_API_TOKEN,
24})

Sanity scales effortlessly to millions of requests without infrastructure management, but every query incurs network costs and latency.

Developer Experience and Type Safety

Both systems offer TypeScript integration, but implementation approaches differ substantially. Payload generates types directly from configuration, while Sanity requires a separate code generation step.

Payload's Configuration-First Types

Types derive automatically from the Payload config, ensuring schema and types stay synchronized:

Advertisement

1// payload-types.ts (auto-generated)
2export interface Post {
3  id: string
4  title: string
5  content: {
6    [k: string]: unknown
7  }[]
8  status: 'draft' | 'published'
9  publishedAt?: string
10  author: string | Author
11}
12
13// Usage in components - fully typed
14async function getPost(id: string): Promise<Post> {
15  const payload = await getPayloadHMR({ config: configPromise })
16  const post = await payload.findByID({
17    collection: 'posts',
18    id,
19  })
20  return post // TypeScript knows the exact shape
21}

Schema changes require restarting the dev server to regenerate types, but the tight coupling prevents type drift.

Sanity's GROQ and Code Generation

Sanity uses GROQ queries with a separate type generation workflow. The sanity-codegen package creates TypeScript definitions from schemas:

1// sanity.types.ts (generated via sanity-codegen)
2export interface Post {
3  _type: 'post'
4  _id: string
5  title: string
6  content: PortableTextBlock[]
7  status: 'draft' | 'published'
8  publishedAt?: string
9  author: Reference<Author>
10}
11
12// Type-safe GROQ queries with groq-builder
13import { q } from 'groq-builder'
14import { client } from './sanity'
15
16const postsQuery = q('*')
17  .filterByType('post')
18  .filter("status == 'published'")
19  .order('publishedAt desc')
20  .slice(0, 20)
21  .grab({
22    _id: true,
23    title: true,
24    excerpt: true,
25  })
26
27const posts = await client.fetch(postsQuery.query)
28// posts is fully typed based on the query shape

GROQ's flexibility enables complex queries and data transformations at the API level, reducing client-side processing. The trade-off: queries need manual type annotations or helper libraries for full type safety.

Content Modeling and Relationships

Relationship handling reveals practical differences in daily development workflows. Payload uses traditional foreign keys and population, while Sanity employs references with projection-based resolution.

Payload's population approach:

1const post = await payload.findByID({
2  collection: 'posts',
3  id: postId,
4  depth: 2, // Populate nested relationships
5})
6
7// post.author is fully populated Author object
8console.log(post.author.name) // Direct access, no additional queries

Sanity requires explicit projection in GROQ queries:

1const post = await client.fetch(`
2  *[_type == "post" && _id == $postId][0] {
3    _id,
4    title,
5    "author": author-> {
6      name,
7      bio
8    }
9  }
10`, { postId })
11
12// Relationships resolved in query, not after fetch

Complex content graphs favor different approaches. Payload's depth-based population simplifies deeply nested structures but can over-fetch data. Sanity's projection-based resolution gives precise control over returned data but requires more query planning.

Deployment and Infrastructure Considerations

Infrastructure requirements diverge sharply. Payload needs a Node.js runtime and database, making it suitable for teams already managing application infrastructure. Vercel, Railway, and Render all support Payload deployments with managed PostgreSQL.

Sanity eliminates infrastructure management entirely—no database provisioning, no scaling concerns, no backup strategies. The hosted studio and Content Lake handle everything, but this convenience comes with vendor lock-in and ongoing subscription costs based on usage tiers.

For applications requiring on-premise deployment or strict data residency requirements, Payload's self-hosted nature provides necessary control. Sanity's cloud-only architecture suits teams prioritizing operational simplicity over infrastructure control.

Real-World Use Case Alignment

E-commerce platforms with complex inventory management benefit from Payload's direct database access and transactional capabilities. Real-time stock updates and order processing integrate naturally with the same database handling content.

Marketing sites with globally distributed audiences leverage Sanity's CDN distribution effectively. Content updates propagate to edge locations within seconds, and the managed infrastructure scales automatically during traffic spikes.

Multi-tenant applications face different constraints with each system. Payload supports database-level tenant isolation, while Sanity requires dataset-per-tenant configurations, increasing complexity and costs at scale.

The choice ultimately depends on existing infrastructure, team capabilities, and scaling requirements. Teams comfortable with database management and requiring sub-10ms query times find Payload's architecture advantageous. Teams prioritizing operational simplicity and global content distribution benefit from Sanity's managed approach.

Advertisement

Share this page

Article by Marcus Rodriguez

Full-stack developer specializing in Next.js and modern React patterns. Creator of several open-source React libraries with over 50k downloads.

Related Content

Continue learning with these related articles