
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeTracking view counts or any auto-incrementing counter when a document is fetched is a tricky problem, especially when you need to avoid race conditions. Let me explain the best approaches for this in Sanity:
Incrementing a counter on every document fetch creates a race condition problem - if multiple users fetch the same document simultaneously, some increments could be lost. This is because read-and-update operations aren't atomic.
The modern approach is to use Sanity Functions to track views server-side. Instead of incrementing on fetch (which happens client-side), you can:
inc() patch operation which is designed to handle concurrent updatesHere's how the increment operation works with the Sanity client:
client
.patch('document-id')
.inc({views: 1}) // Atomically increment by 1
.commit()The .inc() operation is specifically designed to handle race conditions - it performs atomic increments that won't lose counts even with concurrent updates.
Example with Sanity Functions:
// In your Sanity Function (sanity.blueprint.ts)
export default defineBlueprint({
functions: [
defineFunction({
name: 'track-view',
handler: async (event) => {
const {articleId} = event.body
await client
.patch(articleId)
.inc({viewCount: 1})
.commit()
return {success: true}
}
})
]
})
// In your frontend
async function trackView(articleId) {
await fetch('https://your-project.api.sanity.io/v2025-01-01/functions/track-view', {
method: 'POST',
body: JSON.stringify({ articleId })
})
}For production use cases, consider tracking views in a separate analytics system (like Google Analytics, Plausible, or a custom service) rather than in your content documents. This approach:
If you need the data in Sanity, you could:
useCdn: true), your queries are cached, so you wouldn't even know about most views// When a user views an article
async function trackView(articleId) {
// Call your Sanity Function (or own API endpoint)
await fetch('/api/track-view', {
method: 'POST',
body: JSON.stringify({ articleId })
})
}
// In your API endpoint or Sanity Function
async function incrementViewCount(articleId) {
await client
.patch(articleId)
.inc({viewCount: 1}) // Atomic increment - race condition safe!
.commit()
}
// Fetch content separately - no mutations here
async function getArticle(articleId) {
return await client.fetch(
`*[_type == "post" && _id == $id][0]`,
{id: articleId}
)
}The key takeaway: use the atomic .inc() operation for incrementing, and separate your view tracking from content fetching. This ensures you won't lose counts even with concurrent updates, and you maintain good performance for your content queries.
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store