How Wagtail Stores Draft Previews (And Why Your Links Break)

If you’ve ever shared a Wagtail admin preview URL with a colleague — something like /cms/pages/326/edit/preview/ — only to have it return a blank page or error, you’ve run into one of Wagtail’s less-documented behaviors: draft previews are stored in the Django session, not the database.

Understanding why this is the case explains several side effects that tend to surface as apparent bugs.

How Preview Storage Works

When you click “Preview” in the Wagtail page editor, the editor POSTs the current form data to the preview endpoint. The PreviewOnEdit view serializes that form data and writes it into Django’s session under a key like wagtail-preview-{page_id}. The GET request that loads the preview iframe then reads that data back out of the session and renders the page in-memory, without ever writing a draft object to the database.

The view handles three HTTP methods for this lifecycle:

POST  /cms/pages/326/edit/preview/   # serialize form data → session
GET   /cms/pages/326/edit/preview/   # read from session → render
DELETE /cms/pages/326/edit/preview/  # clear session key

The session key itself follows the pattern wagtail-preview-{page_id}, with a default expiration of 24 hours (preview_expiration_timeout = 60 * 60 * 24). You can verify this in the source at wagtail/admin/views/generic/preview.py.

This design was an explicit tradeoff. As the original PR author noted when introducing session-based storage in #3383: avoiding a dedicated database table for transient preview data was the priority, and creating an API endpoint for the same purpose would have been significantly more work.

Practical Consequences

Preview links are user- and session-scoped. Because the data lives in your session, a preview URL only renders correctly for the user who generated it, in the browser session that made the POST. If you log out, the session expires, or you share the URL with someone else, the session key is either missing or belongs to a different session — the preview returns nothing.

The Wagtail userbar omits the “Edit this page” button for unpublished pages. The userbar resolves the edit link by looking up the current page object in the database. For a page that has never been published, no Page database record exists with a live URL, so there’s no reverse link to construct. The draft exists only as serialized form data in the session.

signed_cookies as your session backend will cause failures on large pages. If SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' is set, the entire session is stored in the cookie, which has a 4096-byte limit. Previewing a page with substantial content will exceed that limit and result in a 502 error. The database-backed session backends (db or cached_db) do not have this constraint.

Multiple preview tabs can collide. A regression in Wagtail 2.14 (#7392) demonstrated this directly: if the session key is not namespaced correctly by page type, opening previews for two different page types would cause one to overwrite the other’s session data. The fix was to ensure the key included the content type, not just the page ID.

Implications for Your Setup

If your project uses the database session backend (the default for most deployments), the practical impact is minimal during normal editing. The main operational concern is never treating a preview URL as a stable or shareable reference to draft content. It is a point-in-time rendering tied to a specific session.

For environments where editors need to share in-progress work with reviewers or stakeholders who don’t have Wagtail admin access, the session model is a genuine limitation. The wagtail-headless-preview package takes a different approach for headless setups, storing preview data against a database token that can be shared independently of session state — a reasonable pattern to adapt even in traditional Wagtail deployments if shareable previews are a hard requirement.

Summary: Wagtail draft previews are ephemeral session-scoped renders, not persisted draft objects. The behavior is intentional, with measurable tradeoffs around shareability, session backend compatibility, and userbar functionality. Knowing this prevents a category of support requests that are otherwise hard to reproduce.

Martin Mahner

About the author

Martin Mahner

Martin is an active member and contributor to the Django community where he is mostly known as bartTC. It's likely that you have stumbled over one of his apps or snippets. Besides coding, Martin also has …

View Martin's profile