Concepts & Strategy7 min read

Content Lake Explained: How a Real-Time Content Backend Works

A price changes in your admin panel at 9:02 a.m. and the search results, the category page, and the RAG agent answering support tickets all keep quoting the old number for another six minutes. Nobody deployed a bug.

Published September 14, 2026

A price changes in your admin panel at 9:02 a.m. and the search results, the category page, and the RAG agent answering support tickets all keep quoting the old number for another six minutes. Nobody deployed a bug. The content is correct at the source. The problem is the gap between the write and everywhere that write is supposed to show up, and that gap is where stale prices, ghost inventory, and confidently wrong AI answers live.

Most teams try to close that gap by bolting a real-time layer, a search index, and a vector store onto a database that was never designed to broadcast change. Each addition is a pipeline you now own: incremental indexing, re-embedding, deletion handling, backfill.

This article explains what a real-time content backend actually does, using Sanity's Content Lake as the worked example: how change becomes an event, how clients hear about it, and how the freshness problem stops being something you maintain and becomes a property of the store itself.

What is the Content Lake, and why decouple schema from storage?

The Content Lake is a queryable, schema-aware content store that separates your content model from where content is kept. Your schema lives in code as defineType declarations, versioned in the same repository as the rest of your application, while the content itself lives in the cloud store. That split is the whole point: you can change the model without a storage migration, and you can reshape storage without rewriting the model.

This matters because most content backends fuse the two. In a platform where content types are managed in the admin UI and welded to the records already saved against them, a structural change is a slow, risky operation once you have real volume. Adding a field, splitting a type, or renaming a reference becomes a coordinated migration with downtime risk, so teams stop making those changes and the model calcifies around whatever it was on day one.

Decoupling flips that. Because the schema is code, a model change is a pull request: reviewed, versioned, and rolled back like any other commit. Because storage is a managed, queryable lake rather than a table you own, you are not writing migrations to move data between shapes. In Sanity's framing this is the Model your business pillar of the Content Operating System, the intelligent backend for companies building AI content operations at scale. The backend adapts to how you actually structure your business, rather than forcing your business to fit the shape the vendor shipped. That adaptability is what makes the real-time and automation layers on top of it worth having, because a rigid model gives you nothing interesting to react to.

How does a real-time content backend push changes to clients?

A real-time content backend has to turn a single write into a notification that every reader can act on, without asking each reader to poll. Sanity does this through the Live Content API, which is GA and works in two stages. First, clients listen for lightweight content identifiers called sync tags that map to specific requests. Then they query for the exact content they need and update only when the relevant sync tag signals that something changed. The listen channel carries the fact that something moved; the query carries what it now is.

That separation is what keeps it efficient. You are not streaming full documents to every connected client on every keystroke. A sync tag is small, so the fan-out is cheap, and the heavier query only runs when a tag the client cares about actually fires. The Live Content API requires API version v2021-03-25 or later and is available on all plans, including Free.

Reconnection is where naive real-time systems drop updates. If a client's connection blips during a deploy or a mobile handoff, events that fired in the gap are simply gone, and the client silently falls behind. The Live Content API holds recent events and replays them on reconnect, with a 15-minute retention window on Free and Growth plans and custom retention on Enterprise plans. So a client that disconnects and comes back within the window catches up on what it missed rather than assuming it is current when it is not.

The contrast with preview-oriented real-time is worth drawing. Several platforms offer a strong live preview inside the editor, which updates the person editing. The sync-tag listen-then-query model is built to drive live production traffic, the kind news, sports, and commerce sites serve to real visitors, not only the editor's screen.

How do CDN caching and real-time updates coexist?

Real-time and caching pull in opposite directions, and pretending otherwise is how you ship a system that is fast and wrong. A CDN exists to serve a cached copy so you do not hit the origin on every request. Real-time exists to make sure nobody sees a stale copy. Put them together carelessly and you get exactly the 9:02 a.m. failure from the top of this article: the write landed, but the edge kept serving yesterday.

By default, next-sanity revalidates cached routes in the background. That is a deliberate trade for speed: the visitor gets an instant cached response, and the fresh version is fetched behind the scenes for the next request. The consequence is that some visitors keep seeing the previous content until revalidation finishes, which is fine for a blog and unacceptable for a live score or a price.

When you need the guarantee rather than the eventual, the mechanism is explicit. You deploy an Invalidate Sync Tags Function and set waitFor="function" on the <SanityLive> component. Now a content change fires a sync-tag invalidation through a Function running on Content Lake infrastructure, the cache entry tied to that tag is invalidated, and the component waits for that invalidation to complete before treating the update as live. The edge and the real-time layer share the same sync-tag vocabulary, so cache invalidation is driven by the same events that drive client updates rather than by a guessed time-to-live.

The design lesson generalizes beyond Sanity: a real-time backend is only as honest as its cache invalidation. If your freshness story ends at the origin and hands off to a CDN with a fixed TTL, you have moved the staleness, not removed it.

How does content-change automation work without a service to run?

The moment a real-time backend can tell you something changed, the next question is what runs in response. Translate the new copy, re-moderate the edited comment, notify a downstream system, re-index for search. The traditional answer is a webhook that posts to a service you host, scale, retry, and monitor. That service is where a lot of content infrastructure quietly rots.

Sanity's answer is Functions, which are GA: small, single-purpose pieces of code that run on Sanity's cloud infrastructure and react to changes in content. You author them in TypeScript or JavaScript and deploy them to the Content Lake, where they can read and write the dataset, traverse references with GROQ, and call external services. There is no server of yours in the path.

The trigger model is worth getting exactly right because it is a common source of confusion. Document Function trigger events, set via event.on, are create, update, and delete. The publish event is deprecated: it was shorthand for create plus update with includeAllVersions: true, and it cannot be combined with other events, so new work should use the explicit lifecycle events instead. Being precise here matters, because a Function wired to the wrong event either misses changes or fires twice.

This is the Automate everything pillar in practice, and it is also why the freshness pipeline described later is not extra work. A create, update, or delete event is already a first-class thing the backend emits. Reacting to it, whether to keep a search index current or to kick off enrichment, is a Function you write against an event that already exists, not a change-detection system you build from scratch on top of a database that does not broadcast.

How does one query blend exact filters with fuzzy relevance?

Structured query and semantic search fail in opposite ways, and a real-time backend that feeds an AI agent has to hold both. GROQ, like SQL and GraphQL, lets you write a predicate and get exactly what you asked for in one round trip, including projections, references, and filters. It is precise and fast for structured data. It falls over the moment a user says "something like X" or "the cozy one" or anything else that lives in vibes rather than in fields. Pure vector search has the inverse problem: it is great at vibes and unreliable at hard constraints, so it will happily return the semantically similar item that violates the price cap you actually meant.

GROQ blends the two in a single query. You narrow with predicates first, because filtering has to hold, then rank what is left with score(), for example score(boost([title] match text::query($q), 2), text::semanticSimilarity($q)) followed by order(_score desc). That blends a BM25-style keyword match on the title, weighted 2x because title hits matter more, with a semantic similarity score across the document. One important rule survives every rewrite: text::semanticSimilarity() is only valid as an argument to score(). Semantic search ranks, it does not filter, so you narrow the candidate set with a filter first and rank what remains.

The operational payoff ties back to freshness. What alternatives like pgvector, Elasticsearch, Algolia, or Pinecone plus metadata filters all require, and what the Content Lake handles, is a content pipeline that keeps the search index fresh on every create, update, and delete. Building incremental indexing, re-embedding on change, deletion handling, and backfill yourself is a real project and a class of bug of its own. When retrieval is wired into the content backend, the freshness problem stops being something you maintain.

When should you reach for a managed real-time backend over building one?

You should build your own real-time content backend when real-time is your core product and you have a team to own it forever, and you should reach for a managed one in nearly every other case. The honest version of the build option is not "add websockets." It is: a change-broadcast layer, a search index that stays fresh on every create, update, and delete, deletion handling so removed records actually leave results, backfill for when you change the model, cache invalidation that the edge respects, and reconnection replay so clients that blink do not silently fall behind. Each of those is a system with its own failure modes and its own on-call rotation.

A managed backend collapses that list into properties of the store. With the Content Lake, change is already an event, Functions already react to it on Sanity's infrastructure, the Live Content API already replays missed events on reconnect within the retention window, and GROQ already blends filters with relevance so retrieval rides the same freshness guarantees as everything else. In the Content Operating System framing this is the Power anything pillar: the same real-time, queryable foundation serves a website, a native app, and an AI agent without a bespoke pipeline per surface.

There is a governance dimension too. Sanity's compliance posture includes SOC 2 Type II, GDPR, and regional data residency, which is one posture to reason about rather than the combined surface of a raw database plus a separately hosted vector store. Worth naming carefully: Workflows, which models editorial process as data defined in TypeScript so that a question like "what published without legal review" becomes a single GROQ query, is currently in beta, so treat it as a direction rather than a shipped guarantee. The GA foundation, Content Lake, GROQ, Functions, and the Live Content API, is what the freshness argument in this article rests on.

Real-time content backend: Sanity Content Lake versus common alternatives

FeatureSanityContentfulStrapiHomegrown DB + vector DB
Schema versus storage couplingSchema-as-code with defineType, versioned in the repo; content lives in Content Lake, so you change one without breaking the other.Content types managed in-platform and tied to stored content; structural changes are possible but slow and risky at scale.You own the schema and the database, so model changes mean migrations you write and run yourself.Schema is whatever your migrations enforce; drift between the app model and the table shape is on you to catch.
Real-time updates in productionLive Content API uses a sync-tag listen-then-query model that drives live production traffic, not only editor preview. Requires v2021-03-25 or later.Live Preview updates the editing experience; production real-time on the read side typically means your own polling or webhook plumbing.No managed real-time layer; you build websockets or SSE and operate them alongside the API.Real-time is a subscription layer you design, run, and page on when it breaks.
Reacting to a content changeFunctions run on Content Lake infrastructure, firing on create, update, and delete events (publish is deprecated), reading and writing the dataset.Webhooks post to your endpoint; the compute, retries, and dataset access all live in your own service.Lifecycle hooks run inside your Strapi process, which you host, scale, and monitor.Change data capture or triggers that you wire up and maintain per table.
Keeping a search index freshContent Lake handles the pipeline that keeps the index current on every create, update, and delete, so freshness stops being something you maintain.Sync content to an external search service; incremental indexing and deletion handling are your integration to build.Index sync, re-indexing on change, and backfill are all app code you own.Incremental indexing, re-embedding on change, deletion handling, and backfill are a real project and a class of bug of their own.
Blending structured filters with relevanceOne GROQ query blends filters with score(), boost([field] match text::query($q), 2), and text::semanticSimilarity($q), then order(_score desc).GraphQL fetches fields; relevance ranking and semantic search live in a separate search or vector service.REST or GraphQL plus a bolted-on search engine, joined in application code.Keyword store plus vector store, with the blend and the rank logic hand-rolled across both.
Query shape and round tripsGROQ returns exactly the shape you asked for in one round trip, including projections, references with ->, and filters.GraphQL avoids over-fetching but resolver depth and reference joins can still fan out into multiple requests.REST tends toward over-fetching or N requests; GraphQL is available but you resolve it yourself.Whatever SQL and joins you write, plus a second hop to the vector store.
Operational ownershipManaged backend: uptime, backups, and the freshness pipeline are Sanity's; SOC 2 Type II, GDPR, and data residency apply.Managed SaaS, though external search and preview infrastructure add moving parts you operate.Self-hosted: uptime, backups, scaling, and the real-time layer are all yours to run.You own every layer, including the compliance posture of two data stores instead of one.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.