Back to Blog
Platform Guides

Headless WordPress on Managed Platforms: WP Engine Atlas vs. WordPress VIP vs. Pantheon Front-End Sites

Priya Sharma
37 min read

Why Managed Headless WordPress Matters Right Now

Running headless WordPress on your own infrastructure sounds exciting until you spend three weekends debugging SSR cold starts, fighting with reverse proxy configs, and wondering why your preview URLs return 404s in staging. Managed platforms remove that friction. They give you an opinionated deployment pipeline, handle SSL termination, manage Node.js runtimes, and take care of the CDN layer so you can focus on building the frontend.

Three platforms have emerged as serious contenders in this space: WP Engine Atlas, WordPress VIP, and Pantheon Front-End Sites. Each takes a different architectural approach. Each has different trade-offs around flexibility, cost, developer experience, and performance. This article breaks down all three in enough detail that you can make a decision for your next project without spinning up trial accounts on each one.

I have shipped production headless WordPress builds on all three platforms over the past two years. Some of what follows reflects direct experience; some reflects the current state of documentation and public tooling as of mid-2022. Platform features change fast, so always verify specifics against the latest docs before committing to a contract.

Architectural Overview

Before comparing features, you need to understand what each platform actually provisions when you create a headless WordPress project.

WP Engine Atlas

Atlas pairs a standard WP Engine WordPress environment with a dedicated Node.js container for your JavaScript frontend. The WordPress backend lives on WP Engine’s existing infrastructure (their “User Portal” managed hosting). The Node.js container runs your Next.js, Faust.js, or other framework app. Both environments share the same WP Engine account, and Atlas provides a “blueprint” system for spinning up paired environments from a GitHub repository.

The Node.js runtime is not a generic container. It is purpose-built for Atlas and supports specific Node.js versions (14.x and 16.x at time of writing). Your frontend app gets its own URL (typically something like your-project.wpengine.com for the JS app and a separate URL for the WordPress backend). Atlas handles routing so that the JS app serves all public traffic while the WordPress admin (/wp-admin) remains accessible on the backend URL.

WordPress VIP

WordPress VIP takes a different approach. The platform runs WordPress in a highly locked-down, containerized environment with strict code review requirements. For headless setups, VIP provides a “decoupled” architecture where your Node.js frontend runs in VIP’s own containerized Node.js hosting. This is separate from their WordPress containers but managed under the same VIP dashboard.

VIP’s Node.js hosting supports Next.js as a first-class citizen. They maintain a @wordpress/vip-go CLI and deployment pipeline that handles build, deploy, and rollback. The WordPress backend on VIP is famously opinionated: no arbitrary plugin installs, mandatory code review for theme/plugin changes pushed via Git, and a curated set of allowed PHP functions. This strictness extends to the headless setup, where you are expected to follow VIP’s patterns for data fetching and caching.

Pantheon Front-End Sites

Pantheon’s approach to headless WordPress arrived later than the other two. Front-End Sites is Pantheon’s answer to the decoupled architecture question. It pairs a Pantheon WordPress (or Drupal) backend with a frontend hosted on Pantheon’s edge infrastructure. Under the hood, Pantheon partnered with a third-party edge compute platform to run the JavaScript frontend, rather than building their own Node.js hosting from scratch.

The frontend deployment uses a Git-based workflow similar to Pantheon’s traditional WordPress hosting. You connect a GitHub or GitLab repository, and Pantheon builds and deploys your Next.js or Gatsby app on every push. The WordPress backend runs on Pantheon’s standard WebOps infrastructure with their Dev/Test/Live environment model.

Next.js Support: ISR, App Router, and Framework Compatibility

Next.js is the dominant framework for headless WordPress frontends. How well each platform supports its features matters enormously.

Atlas and Next.js

Atlas supports Next.js, but the experience is shaped by Faust.js, WP Engine’s open-source framework built on top of Next.js. Faust.js provides WordPress-specific utilities: authentication helpers, a template hierarchy that mirrors WordPress’s own, and built-in support for WPGraphQL. If you use Faust.js, you get a smoother experience on Atlas. If you want to run a vanilla Next.js app, it works but you lose some of the Atlas-specific optimizations.

ISR (Incremental Static Regeneration) works on Atlas, but with caveats. Atlas uses its own caching layer in front of the Node.js container. When you set a revalidate value in getStaticProps, the ISR behavior depends on how Atlas’s edge cache interacts with Next.js’s internal cache. In practice, this means you sometimes see stale content longer than your revalidate interval would suggest, because the edge cache TTL and the ISR revalidation timer operate independently.

As for the App Router (introduced in Next.js 13), Atlas support was initially limited. The platform was built around the Pages Router paradigm, and Faust.js was designed entirely for Pages Router patterns. App Router support has been gradually improving, but if you are planning a project that relies heavily on React Server Components and the app/ directory structure, test thoroughly on Atlas before committing.

Here is a typical next.config.js for an Atlas project using Faust.js:

// next.config.js for WP Engine Atlas + Faust.js
const { withFaust } = require("@faustwp/core");

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: [
      process.env.NEXT_PUBLIC_WORDPRESS_URL?.replace("https://", "").replace(
        "http://",
        ""
      ),
    ],
  },
  i18n: {
    locales: ["en"],
    defaultLocale: "en",
  },
};

module.exports = withFaust(nextConfig);

VIP and Next.js

WordPress VIP has strong Next.js support. Their decoupled Node.js hosting is essentially a managed Next.js runtime, and they have invested in ensuring ISR works reliably. VIP’s caching infrastructure is designed to respect Next.js cache headers, so ISR revalidation behaves more predictably than on some other platforms.

VIP supports both Pages Router and App Router, though their documentation and example repositories lean toward Pages Router patterns as of mid-2022. The platform’s Node.js hosting runs recent LTS versions of Node, and they actively test against new Next.js releases.

One significant advantage of VIP’s Next.js support is their integration with their own caching infrastructure. VIP uses a sophisticated page cache that understands Next.js responses. When ISR revalidates a page, VIP’s cache layer can serve the stale version while the new one generates, then atomically swap in the fresh content. This reduces the “flash of stale content” problem that plagues some ISR implementations.

// next.config.js for WordPress VIP decoupled setup
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: [
      "your-site.wpvip.com",
      // VIP image CDN domain
      "your-site.go-vip.net",
    ],
  },
  async rewrites() {
    return [
      {
        source: "/wp-admin/:path*",
        destination: `${process.env.WORDPRESS_URL}/wp-admin/:path*`,
      },
    ];
  },
  // VIP recommends explicit output configuration
  output: "standalone",
};

module.exports = nextConfig;

Pantheon and Next.js

Pantheon Front-End Sites supports Next.js and Gatsby. Their Next.js support covers static generation and server-side rendering. ISR support exists but has some limitations tied to the edge infrastructure they use. Because Pantheon’s frontend hosting runs on edge functions rather than a persistent Node.js server, long-running ISR revalidation can behave differently than on a traditional Node.js host.

The App Router is supported, though Pantheon’s starter kits and documentation focus on Pages Router. One thing Pantheon does well is environment parity: their Dev/Test/Live model extends to frontend sites, so you get three environments with separate builds and separate environment variables. This is genuinely useful for testing ISR behavior in a staging context before promoting to production.

// next.config.js for Pantheon Front-End Sites
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: [
      process.env.WORDPRESS_HOST || "dev-your-site.pantheonsite.io",
      "live-your-site.pantheonsite.io",
    ],
  },
  // Pantheon recommends trailing slash consistency
  trailingSlash: true,
  // Output mode depends on Pantheon's build pipeline
  output: "standalone",
};

module.exports = nextConfig;

Content Preview Implementation

Preview is one of the hardest problems in headless WordPress. Content editors expect to click “Preview” in wp-admin and see their draft rendered in the frontend theme. Each platform handles this differently.

Atlas Preview with Faust.js

Faust.js includes a built-in preview system that handles the OAuth-like handshake between WordPress and the Next.js frontend. When an editor clicks “Preview,” Faust.js redirects the request to a Next.js API route that authenticates the user, fetches the draft content via WPGraphQL, and renders the preview page. The implementation uses a combination of WordPress application passwords and a custom authorization flow.

Here is what the preview API route looks like in a Faust.js project:

// pages/api/faust/[[...route]].js
import { apiRouter } from "@faustwp/core";

export default apiRouter;

// The Faust.js apiRouter handles:
// - /api/faust/preview (preview redirects from WordPress)
// - /api/faust/token (token exchange for authenticated requests)
// - /api/faust/logout (clearing auth tokens)

On the WordPress side, Faust.js requires the FaustWP plugin, which rewrites the preview URL to point to your Atlas frontend. The setup works well when everything is configured correctly, but debugging authentication failures can be frustrating because the token exchange involves multiple redirects.

VIP Preview

WordPress VIP provides a preview mechanism that integrates with their platform’s authentication system. Since VIP controls both the WordPress and Node.js environments, they can establish a trusted connection between the two without relying on application passwords or OAuth flows.

VIP’s preview implementation typically works through a preview API route in your Next.js app that receives a signed preview token from WordPress. The token is verified against VIP’s authentication service, and the draft content is fetched via the WordPress REST API or WPGraphQL.

// pages/api/preview.js (VIP-style preview handler)
export default async function handler(req, res) {
  const { post_id, token, post_type } = req.query;

  // Verify the preview token against VIP's auth service
  const isValid = await verifyVipPreviewToken(token, post_id);

  if (!isValid) {
    return res.status(401).json({ message: "Invalid preview token" });
  }

  // Enable Next.js preview mode
  res.setPreviewData({
    postId: post_id,
    postType: post_type || "post",
    token: token,
  });

  // Redirect to the post's URL
  const postPath = await resolvePostPath(post_id, post_type);
  res.redirect(postPath);
}

async function verifyVipPreviewToken(token, postId) {
  const response = await fetch(
    `${process.env.WORDPRESS_URL}/wp-json/vip/v1/preview/verify`,
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );
  return response.ok;
}

async function resolvePostPath(postId, postType) {
  const response = await fetch(
    `${process.env.WORDPRESS_URL}/wp-json/wp/v2/${postType}s/${postId}`
  );
  const post = await response.json();
  return new URL(post.link).pathname;
}

Pantheon Preview

Pantheon’s preview implementation is less opinionated than the other two. Because Pantheon’s Front-End Sites product is newer, their preview documentation relies more on community patterns. The typical approach involves a WordPress plugin that rewrites preview URLs to point to your Pantheon-hosted frontend, combined with a Next.js preview API route that fetches draft content.

Pantheon’s multi-environment model is actually an advantage here. You can configure preview to always hit the Dev or Test frontend environment, keeping your Live environment clean. This means editors preview against a dedicated preview build, reducing the risk of preview-related bugs affecting production.

// pages/api/preview.js (Pantheon-style)
export default async function handler(req, res) {
  const { secret, id, slug, post_type } = req.query;

  // Check the secret to confirm this is a valid preview request
  if (secret !== process.env.PREVIEW_SECRET) {
    return res.status(401).json({ message: "Invalid secret" });
  }

  if (!id) {
    return res.status(400).json({ message: "Missing post ID" });
  }

  // Fetch the post to verify it exists
  const apiBase = post_type === "page" ? "pages" : "posts";
  const response = await fetch(
    `${process.env.WORDPRESS_URL}/wp-json/wp/v2/${apiBase}/${id}?_fields=id,slug,status`,
    {
      headers: {
        Authorization: `Basic ${Buffer.from(
          `${process.env.WP_USER}:${process.env.WP_APP_PASSWORD}`
        ).toString("base64")}`,
      },
    }
  );

  if (!response.ok) {
    return res.status(404).json({ message: "Post not found" });
  }

  const post = await response.json();

  // Enable preview mode and redirect
  res.setPreviewData({
    postId: id,
    postType: post_type || "post",
  });

  const redirectSlug = slug || post.slug;
  const basePath = post_type === "page" ? "" : "/blog";
  res.redirect(`${basePath}/${redirectSlug}`);
}

WPGraphQL Integration and Caching

WPGraphQL is the de facto standard for querying WordPress data in headless setups. How each platform integrates with it and caches GraphQL responses has a direct impact on performance and developer experience.

Atlas + WPGraphQL

WPGraphQL is a first-class citizen on Atlas. WP Engine employs Jason Bahl, the creator of WPGraphQL, and the plugin is pre-installed on Atlas environments. The integration is tight: Atlas’s caching layer understands WPGraphQL responses and can cache them at the edge. WP Engine also develops Smart Cache invalidation for WPGraphQL, which means that when you update a post, only the cached queries that reference that post get purged.

This is a significant advantage. On a busy site with hundreds of pages, smart cache invalidation means your ISR revalidation does not need to re-fetch all queries from the database. Only the affected queries get busted.

Here is a typical GraphQL query for fetching posts on Atlas:

// lib/queries.js (Atlas / Faust.js project)
import { gql } from "@apollo/client";

export const GET_POSTS = gql`
  query GetPosts($first: Int = 10, $after: String) {
    posts(first: $first, after: $after) {
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        id
        databaseId
        title
        excerpt
        slug
        date
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
        categories {
          nodes {
            name
            slug
          }
        }
        author {
          node {
            name
            avatar {
              url
            }
          }
        }
      }
    }
  }
`;

export const GET_POST_BY_SLUG = gql`
  query GetPostBySlug($slug: ID!) {
    post(id: $slug, idType: SLUG) {
      id
      databaseId
      title
      content
      date
      modified
      seo {
        title
        metaDesc
        opengraphImage {
          sourceUrl
        }
      }
      featuredImage {
        node {
          sourceUrl
          altText
          mediaDetails {
            width
            height
          }
        }
      }
      author {
        node {
          name
          description
          avatar {
            url
          }
        }
      }
      categories {
        nodes {
          name
          slug
        }
      }
      tags {
        nodes {
          name
          slug
        }
      }
    }
  }
`;

VIP + WPGraphQL

WordPress VIP supports WPGraphQL, but with their characteristic caution. VIP’s code review process means any WPGraphQL customizations (custom types, resolvers, or schema modifications) need to pass their review. This adds friction but also ensures that your GraphQL schema does not introduce N+1 query problems or other performance pitfalls.

VIP’s caching layer does not have the same WPGraphQL-specific smart invalidation that Atlas offers. Instead, VIP relies on their general page cache and object cache (which uses Memcached). GraphQL responses are cached at the application level using object caching, and you can leverage VIP’s wpcom_vip_cache_full_page-style caching for full-page SSR responses.

One pattern VIP recommends is using persisted queries. Instead of sending the full GraphQL query string with every request, you register queries ahead of time and reference them by hash. This reduces payload size and allows VIP’s caching infrastructure to cache query results more efficiently.

// lib/graphql-client.js (VIP project with persisted queries)
const PERSISTED_QUERIES = {
  GetPosts: "sha256:a1b2c3d4e5f6...",
  GetPostBySlug: "sha256:f6e5d4c3b2a1...",
};

export async function fetchGraphQL(queryName, variables = {}) {
  const queryId = PERSISTED_QUERIES[queryName];

  if (!queryId) {
    throw new Error(`No persisted query found for: ${queryName}`);
  }

  const response = await fetch(
    `${process.env.WORDPRESS_URL}/graphql`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        queryId: queryId,
        variables: variables,
      }),
      // VIP recommends explicit cache control
      next: {
        revalidate: 60,
        tags: [`graphql-${queryName}`],
      },
    }
  );

  if (!response.ok) {
    throw new Error(`GraphQL request failed: ${response.status}`);
  }

  const json = await response.json();

  if (json.errors) {
    console.error("GraphQL errors:", json.errors);
    throw new Error("GraphQL query returned errors");
  }

  return json.data;
}

Pantheon + WPGraphQL

Pantheon supports WPGraphQL without the tight integration that Atlas offers or the review requirements that VIP imposes. You install WPGraphQL on your Pantheon WordPress site like any other plugin, configure it, and query it from your frontend. Pantheon’s caching for GraphQL sits at the platform’s CDN level, using standard HTTP cache headers.

This means you have more control over caching behavior but also more responsibility. You need to set appropriate Cache-Control headers on your GraphQL endpoint, and you need to handle cache invalidation yourself. Pantheon’s global CDN will cache GET-based GraphQL queries if you configure the headers correctly, but POST-based queries (which is the default for most GraphQL clients) will bypass the CDN cache.

To get CDN caching for GraphQL on Pantheon, you need to use GET requests with query parameters:

// lib/graphql-client.js (Pantheon project with GET-based caching)
export async function fetchGraphQL(query, variables = {}) {
  const params = new URLSearchParams({
    query: query,
    variables: JSON.stringify(variables),
  });

  // Use GET requests so Pantheon's CDN can cache responses
  const response = await fetch(
    `${process.env.WORDPRESS_URL}/graphql?${params.toString()}`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
      // Respect Pantheon's Surrogate-Key headers for targeted purging
      next: {
        revalidate: 300,
      },
    }
  );

  if (!response.ok) {
    throw new Error(`GraphQL fetch failed: ${response.status}`);
  }

  const json = await response.json();
  return json.data;
}

// For mutations or authenticated requests, fall back to POST
export async function mutateGraphQL(mutation, variables = {}) {
  const response = await fetch(
    `${process.env.WORDPRESS_URL}/graphql`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.WP_AUTH_TOKEN}`,
      },
      body: JSON.stringify({
        query: mutation,
        variables: variables,
      }),
    }
  );

  const json = await response.json();
  return json.data;
}

Build and Deployment Workflows

The deployment pipeline is where managed platforms earn their keep. Here is how each one handles the build-and-ship process.

Atlas Deployment

Atlas uses a GitHub-connected deployment model. You push code to a branch, and Atlas picks up the change, runs the build, and deploys the result. Atlas provides “blueprints,” which are repository templates that include both the WordPress configuration (as a wp-content directory) and the JavaScript frontend in a monorepo structure.

A typical Atlas blueprint repository looks like this:

# Atlas blueprint repo structure
project-root/
├── wp-content/
│   ├── plugins/
│   │   └── my-custom-plugin/
│   ├── themes/
│   │   └── my-headless-theme/
│   └── mu-plugins/
├── frontend/
│   ├── pages/
│   ├── components/
│   ├── lib/
│   ├── styles/
│   ├── next.config.js
│   ├── package.json
│   └── .env.local.sample
├── .wpengine/
│   └── config.yml
└── README.md

The .wpengine/config.yml file controls the build:

# .wpengine/config.yml
version: 1
build:
  install: npm install --prefix frontend
  build: npm run build --prefix frontend
  output: frontend/.next
node:
  version: "16"
environment:
  variables:
    - NEXT_PUBLIC_WORDPRESS_URL
    - FAUST_SECRET_KEY

The build process runs on WP Engine’s build infrastructure, which means build times depend on their capacity. In my experience, builds take between 2 and 5 minutes for a typical Next.js project. Rollbacks are supported: Atlas keeps previous deployments and lets you roll back to any recent one from the dashboard.

VIP Deployment

VIP’s deployment workflow is the most structured of the three. Code changes go through VIP’s Git-based system, and every push triggers a build. For the decoupled Node.js frontend, VIP provides a separate Git repository or a monorepo structure where the frontend lives alongside the WordPress code.

VIP’s build pipeline is notable for its review requirements. Production deployments on VIP Go require a code review, and VIP’s team may review your code before it ships. For the Node.js frontend, the process is somewhat lighter, but VIP still enforces quality gates.

# VIP monorepo structure
project-root/
├── wordpress/
│   ├── plugins/
│   ├── themes/
│   │   └── my-headless-theme/
│   └── client-mu-plugins/
│       └── plugin-loader.php
├── decoupled/
│   ├── app/          # or pages/ for Pages Router
│   ├── components/
│   ├── lib/
│   ├── next.config.js
│   ├── package.json
│   └── Dockerfile    # VIP uses containers
├── .vip/
│   └── config.yml
└── package.json      # root workspace config

VIP’s build configuration:

# .vip/config.yml
version: 2
app:
  type: nodejs
  node_version: "18"
  build:
    commands:
      - npm ci --prefix decoupled
      - npm run build --prefix decoupled
  runtime:
    command: npm start --prefix decoupled
    port: 3000
  health_check:
    path: /api/health
    interval: 30
    timeout: 5

Build times on VIP are generally fast (under 3 minutes in my experience), and VIP’s infrastructure is designed for zero-downtime deployments. New builds spin up in a new container, health checks pass, and traffic shifts over. If the health check fails, the deployment rolls back automatically.

Pantheon Deployment

Pantheon Front-End Sites uses a Git-based workflow with their Multidev environments. Each branch gets its own environment (up to a limit depending on your plan), which is excellent for testing. The build pipeline runs on Pantheon’s infrastructure, and you configure it through a pantheon.yml or similar configuration file.

# pantheon.yml (extended for Front-End Sites)
api_version: 1

build_step: true

workflows:
  build:
    steps:
      - type: node
        node_version: "18"
        commands:
          - npm ci
          - npm run build
        output_directory: .next

# Front-End Sites specific
frontend:
  framework: nextjs
  build_command: npm run build
  output_directory: .next
  environment_variables:
    - WORDPRESS_URL
    - PREVIEW_SECRET
    - NEXT_PUBLIC_SITE_URL

Pantheon’s Dev/Test/Live promotion model is where their deployment story shines. You deploy to Dev, test, promote to Test for staging review, then promote to Live. Each promotion is a fast, atomic operation because the build artifact is already compiled. This is faster and safer than rebuilding at each stage.

The downside is that Pantheon’s build infrastructure can be slower for initial builds, especially for large Next.js projects. I have seen build times of 5 to 8 minutes for projects with heavy dependencies. Subsequent builds benefit from caching but are still not as fast as VIP’s.

Environment Variables and Secrets

Handling sensitive configuration (API keys, preview secrets, Stripe tokens) differs meaningfully across platforms.

Atlas Environment Variables

Atlas manages environment variables through the WP Engine User Portal. You set them per environment (production, staging, development), and they are available to your Node.js app at build time and runtime. Atlas distinguishes between build-time variables (prefixed with NEXT_PUBLIC_ for Next.js) and runtime-only variables.

One limitation: Atlas does not currently support secrets management in the way a tool like HashiCorp Vault or AWS Secrets Manager would. Your environment variables are stored in WP Engine’s system and passed to the container. For most WordPress projects, this is sufficient, but if you have strict compliance requirements, you may need an external secrets manager.

VIP Environment Variables

VIP provides environment variable management through their VIP Dashboard and CLI. They support both application-level variables (available to your Node.js app) and WordPress-level constants. VIP also provides some built-in variables like VIP_GO_ENV that tell your app which environment it is running in (production, staging, development).

VIP’s handling of secrets is more mature. They offer a secrets management feature for sensitive values, and their platform encrypts environment variables at rest. For teams working under SOC 2 or HIPAA-adjacent requirements, this matters.

# Setting env vars via VIP CLI
vip app env var set --app=your-app --env=production \
  WORDPRESS_URL="https://your-site.wpvip.com" \
  GRAPHQL_ENDPOINT="/graphql" \
  PREVIEW_SECRET="your-secret-here"

# Listing current env vars
vip app env var list --app=your-app --env=production

Pantheon Environment Variables

Pantheon’s approach to environment variables for Front-End Sites follows their standard pattern: you set them per environment through the Pantheon Dashboard or Terminus CLI. Each environment (Dev, Test, Live, and Multidev branches) can have its own set of variables.

# Setting env vars via Terminus for Pantheon
terminus env:set your-site.live -- \
  WORDPRESS_URL="https://live-your-site.pantheonsite.io" \
  PREVIEW_SECRET="your-secret-here" \
  NEXT_PUBLIC_SITE_URL="https://www.yoursite.com"

Pantheon does not have a dedicated secrets management layer. Environment variables are stored in their platform and available to the build process and runtime. For projects that need more granular secrets management, you would integrate a third-party solution.

Performance: Edge Caching, CDN, and SSR/SSG Trade-offs

Performance is the main reason teams go headless. Here is how each platform delivers on that promise.

Atlas Performance

Atlas leverages WP Engine’s Global Edge Security network (powered by Cloudflare) for CDN and edge caching. Static assets and statically generated pages are served from the edge. SSR responses pass through the CDN but are typically not cached at the edge unless you configure specific cache headers.

For SSG pages, Atlas performance is excellent. Pages generated at build time are served directly from the CDN with sub-100ms TTFB in most regions. ISR pages have a slightly higher TTFB on the first request after revalidation (200-400ms) because the request must reach the Node.js container, but subsequent requests are fast.

SSR pages (using getServerSideProps) are the slowest scenario on Atlas. Every request hits the Node.js container, which then queries the WordPress backend via WPGraphQL. Depending on query complexity, TTFB for SSR pages typically ranges from 300ms to 1.2 seconds. The WPGraphQL smart cache helps here, because frequently-requested data is often already in the WordPress object cache.

VIP Performance

WordPress VIP has the most mature caching infrastructure of the three. Their CDN uses a multi-layer caching strategy: edge cache (CDN), application cache (Varnish-like), and object cache (Memcached). For headless setups, the Node.js frontend benefits from VIP’s edge caching for static responses and their application-level caching for SSR responses.

VIP’s performance advantage is most visible on high-traffic sites. Their infrastructure is designed for sites that serve millions of page views per month (think TechCrunch, Rolling Stone, and similar publications). The caching layers are aggressive and well-tuned, which means ISR and SSG pages are fast consistently, not just on average.

SSR performance on VIP is generally better than on Atlas because VIP’s WordPress backend is heavily optimized. Their object cache hit rates are typically above 90%, which means GraphQL queries resolve quickly even when they are not cached at the frontend level.

Typical TTFB ranges on VIP:

  • SSG pages: 20-60ms (served from edge)
  • ISR pages (cached): 30-80ms
  • ISR pages (revalidating): 150-300ms
  • SSR pages: 200-600ms

Pantheon Performance

Pantheon’s Global CDN (powered by Fastly) serves static assets and cached pages from edge locations worldwide. For Front-End Sites, the edge infrastructure also handles SSR and ISR responses, which gives Pantheon a latency advantage in some scenarios because the rendering happens closer to the user.

The edge rendering approach has trade-offs. Cold starts can be slower than a persistent Node.js server because the edge function needs to spin up. For SSG and ISR-cached pages, this is not an issue because the response is already generated. But for SSR or ISR-revalidation requests, the cold start penalty can add 200-500ms in the worst case.

Pantheon’s advantage is their Surrogate-Key-based cache invalidation via Fastly. When a WordPress post is updated, Pantheon can surgically purge only the CDN cache entries that reference that post. This is similar to Atlas’s WPGraphQL smart cache invalidation but operates at the CDN level rather than the application level.

Typical TTFB ranges on Pantheon:

  • SSG pages: 30-80ms (served from edge)
  • ISR pages (cached): 40-90ms
  • ISR pages (revalidating): 200-500ms (includes potential cold start)
  • SSR pages: 300-900ms (edge function execution)

Cost Comparison

Pricing is one of the biggest differentiators, and the ranges are wide.

WP Engine Atlas

Atlas pricing starts at roughly $25/month for a starter plan (one environment, limited bandwidth). The Growth plan, which most professional projects need, runs $60-115/month. This includes the WordPress hosting and the Node.js hosting bundled together. Additional costs come from bandwidth overages and additional environments.

For agencies running multiple headless sites, Atlas provides account-level pricing that can bring per-site costs down. WP Engine’s pricing page changes frequently, so verify current rates, but the entry point is the most accessible of the three platforms.

WordPress VIP

WordPress VIP is the premium option. Their pricing starts at roughly $2,000-5,000/month for the base tier, and enterprise plans run $5,000-25,000/month or more. This includes the WordPress hosting, Node.js hosting, code review, 24/7 support, and SLA guarantees.

VIP’s pricing makes sense for large publishers, enterprise brands, and organizations where downtime costs more per hour than the monthly platform fee. For a small agency or startup, VIP is almost certainly over-budget. However, VIP does offer a “VIP Start” tier that is more accessible, though it comes with fewer features.

Pantheon Front-End Sites

Pantheon’s pricing for Front-End Sites is bundled with their WebOps platform plans. The Performance tier (which supports Front-End Sites) starts at roughly $175-300/month. Higher tiers with more environments, more traffic, and more features run $500-2,000/month.

Pantheon sits in the middle ground between Atlas and VIP. Their pricing is reasonable for mid-size projects, and the Dev/Test/Live environment model provides value that justifies the cost for teams that need staging workflows.

Cost Summary Table

Here is a rough comparison for a single headless WordPress site with moderate traffic (50,000-200,000 page views/month):

Platform Entry Price Typical Production Cost Enterprise Tier
WP Engine Atlas ~$25/mo $60-115/mo $200-600/mo
WordPress VIP ~$2,000/mo $2,500-5,000/mo $5,000-25,000/mo
Pantheon FES ~$175/mo $300-600/mo $1,000-2,000/mo

Faust.js vs. Custom Next.js Setups

WP Engine’s Faust.js framework deserves its own section because it represents a philosophical choice: use an opinionated WordPress-specific framework, or build your own abstraction layer.

What Faust.js Gives You

Faust.js provides a WordPress template hierarchy for Next.js. If you know WordPress theming, you know that a request for a single blog post resolves to single-post.php, or single.php, or index.php in a fallback chain. Faust.js recreates this in Next.js: you define templates in a wp-templates/ directory, and Faust’s routing layer picks the right one based on the WordPress content type.

// wp-templates/single.js (Faust.js template)
import { gql } from "@apollo/client";
import { Header, Footer, ContentWrapper, PostHeader } from "../components";

export default function SinglePost(props) {
  const { title, content, date, author, featuredImage, categories } =
    props.data.post;

  return (
    <>
      <Header />
      <ContentWrapper>
        <article>
          <PostHeader
            title={title}
            date={date}
            author={author.node}
            image={featuredImage?.node}
            categories={categories?.nodes}
          />
          <div
            className="prose max-w-none"
            dangerouslySetInnerHTML={{ __html: content }}
          />
        </article>
      </ContentWrapper>
      <Footer />
    </>
  );
}

SinglePost.query = gql`
  query GetPost($databaseId: ID!) {
    post(id: $databaseId, idType: DATABASE_ID) {
      title
      content
      date
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
      featuredImage {
        node {
          sourceUrl
          altText
        }
      }
      categories {
        nodes {
          name
          slug
        }
      }
    }
  }
`;

SinglePost.variables = ({ databaseId }) => ({
  databaseId,
});

Faust.js also handles authentication (for preview and protected content), WordPress block rendering (converting Gutenberg blocks to React components), and SEO meta tag generation from Yoast or RankMath data.

When Custom Next.js Makes More Sense

Faust.js ties you to specific patterns. If your frontend needs to pull data from multiple sources (WordPress plus a headless CMS for some content, plus a product API, plus a third-party service), Faust’s WordPress-centric routing becomes a constraint rather than a help. You end up fighting the framework instead of using it.

Custom Next.js setups also give you more control over data fetching. With Faust.js, queries are coupled to templates. In a custom setup, you can compose queries, use React Query or SWR for client-side data, and structure your data layer however you want.

Here is what a custom Next.js setup looks like for the same single post page:

// app/blog/[slug]/page.js (Custom Next.js App Router)
import { getPostBySlug, getAllPostSlugs } from "@/lib/wordpress";
import { PostHeader } from "@/components/PostHeader";
import { PostContent } from "@/components/PostContent";
import { notFound } from "next/navigation";

export async function generateStaticParams() {
  const slugs = await getAllPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

export async function generateMetadata({ params }) {
  const post = await getPostBySlug(params.slug);
  if (!post) return {};

  return {
    title: post.seo?.title || post.title,
    description: post.seo?.metaDesc || post.excerpt,
    openGraph: {
      title: post.seo?.title || post.title,
      description: post.seo?.metaDesc,
      images: post.featuredImage
        ? [{ url: post.featuredImage.node.sourceUrl }]
        : [],
    },
  };
}

export default async function BlogPost({ params }) {
  const post = await getPostBySlug(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <PostHeader
        title={post.title}
        date={post.date}
        author={post.author.node}
        categories={post.categories.nodes}
        image={post.featuredImage?.node}
      />
      <PostContent content={post.content} />
    </article>
  );
}

// Revalidate every 5 minutes
export const revalidate = 300;
// lib/wordpress.js (Custom data fetching layer)
const GRAPHQL_ENDPOINT = `${process.env.WORDPRESS_URL}/graphql`;

async function fetchAPI(query, variables = {}) {
  const response = await fetch(GRAPHQL_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query, variables }),
  });

  const json = await response.json();

  if (json.errors) {
    console.error("WordPress GraphQL Error:", json.errors);
    throw new Error("Failed to fetch WordPress data");
  }

  return json.data;
}

export async function getPostBySlug(slug) {
  const data = await fetchAPI(
    `
    query PostBySlug($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        title
        content
        excerpt
        date
        modified
        slug
        author {
          node {
            name
            avatar { url }
          }
        }
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails { width height }
          }
        }
        categories {
          nodes { name slug }
        }
        seo {
          title
          metaDesc
          opengraphImage { sourceUrl }
        }
      }
    }
  `,
    { slug }
  );

  return data?.post;
}

export async function getAllPostSlugs() {
  const data = await fetchAPI(`
    query AllSlugs {
      posts(first: 1000, where: { status: PUBLISH }) {
        nodes {
          slug
        }
      }
    }
  `);

  return data?.posts?.nodes?.map((node) => node.slug) || [];
}

The custom approach requires more initial setup, but it scales better for complex projects and does not lock you into WP Engine’s ecosystem.

Verdict on Faust.js

Use Faust.js if your project is WordPress-only, you are on Atlas, and you want to ship fast. Use a custom Next.js setup if you need flexibility, plan to use App Router features, or might migrate away from WP Engine in the future. Faust.js is a good accelerator, not a long-term architecture for every project.

When Headless Makes Sense vs. Traditional WordPress

Not every WordPress project benefits from going headless. The added complexity of a decoupled architecture has real costs: more infrastructure to manage, a separate deployment pipeline for the frontend, preview workflows that need custom implementation, and a steeper learning curve for content editors accustomed to the WordPress admin preview.

Go Headless When:

Your frontend team is JavaScript-first. If your developers are more comfortable with React, Vue, or Svelte than PHP, headless lets them work in their preferred stack. The productivity gains from using familiar tools often outweigh the complexity costs.

You need to pull content from multiple sources. Headless WordPress shines when WordPress is one of several data sources. A marketing site that combines WordPress blog content with product data from Shopify, event data from an API, and documentation from a docs platform benefits enormously from a unified frontend that fetches from all these sources.

Performance is a non-negotiable requirement. Static generation and ISR deliver performance that traditional WordPress cannot match, even with aggressive caching plugins. If your site needs sub-100ms TTFB globally, headless with edge-cached static pages is the way to get there.

You are building a multi-channel content strategy. If the same WordPress content powers a website, a mobile app, a kiosk display, and an email newsletter, the headless API-first approach makes content truly reusable. Traditional WordPress themes render HTML; headless WordPress delivers structured data that any client can consume.

Security requirements demand a minimal attack surface. A headless frontend with a locked-down WordPress backend (accessible only via API, not publicly exposed) significantly reduces the attack surface. There is no public-facing PHP to exploit, no plugin vulnerabilities exposed to the internet.

Stick with Traditional WordPress When:

Your content team relies on the WordPress editor experience. Gutenberg’s block editor, pattern library, and full-site editing are powerful tools for content creators. Going headless means your frontend will not reflect Gutenberg’s visual editing experience. Content editors lose the “what you see is what you get” workflow.

You use plugins that depend on frontend rendering. Many WordPress plugins (form builders, e-commerce plugins like WooCommerce, membership plugins) assume they control the frontend HTML. Going headless means rebuilding that functionality in your JavaScript framework or finding headless-compatible alternatives.

Your team is small and does not have dedicated frontend developers. A solo developer or a small agency can build and maintain a traditional WordPress theme faster than a headless architecture. The overhead of maintaining two codebases (WordPress backend plus JavaScript frontend) is significant.

Budget is tight. Traditional WordPress hosting costs $5-50/month. Managed headless platforms start at $25/month (Atlas) and go up to thousands (VIP). If the project budget does not justify the platform cost, traditional WordPress with a good caching plugin will serve most needs.

The project is a simple content site. A blog, a small business site, or a portfolio does not need headless architecture. The added complexity is not justified by the benefits. WordPress’s traditional rendering is fast enough, flexible enough, and far simpler to maintain.

Platform Comparison Summary

Let me bring the comparison together with a decision framework.

Choose WP Engine Atlas If:

  • You want the lowest entry price for managed headless WordPress.
  • Your team is comfortable with Faust.js or willing to learn it.
  • You need WPGraphQL smart cache invalidation out of the box.
  • You are already a WP Engine customer and want to add headless to an existing site.
  • Your project is primarily WordPress-driven (not pulling from many external APIs).

Choose WordPress VIP If:

  • You are a large publisher or enterprise brand with significant traffic.
  • Uptime SLAs and 24/7 support with guaranteed response times are requirements.
  • Your organization has compliance requirements (SOC 2, data residency).
  • You value the code review process as a quality gate.
  • Budget is not the primary constraint.

Choose Pantheon Front-End Sites If:

  • You value the Dev/Test/Live environment model for staging workflows.
  • Your team already uses Pantheon for WordPress or Drupal hosting.
  • You want Multidev environments for branch-based testing.
  • You need a middle ground between Atlas’s simplicity and VIP’s enterprise features.
  • Your project is a Drupal-to-WordPress migration that needs a familiar deployment model.

Practical Migration Strategy: Moving to Headless on a Managed Platform

If you have decided to go headless and picked a platform, here is a practical approach to migration that minimizes risk.

Step 1: Set Up WPGraphQL on Your Existing Site

Before touching the frontend, install WPGraphQL on your current WordPress site and verify that all your content is accessible via the GraphQL API. Test queries for posts, pages, custom post types, menus, and any custom fields (ACF, custom meta).

# Install WPGraphQL (via WP CLI on any platform)
wp plugin install wp-graphql --activate

# If using ACF, also install the WPGraphQL for ACF extension
wp plugin install wpgraphql-acf --activate

# Verify the GraphQL endpoint is accessible
curl -X POST https://your-site.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ generalSettings { title description } }"}'

Step 2: Build the Frontend Locally

Create your Next.js project locally and build out the key pages: homepage, blog listing, single post, single page. Fetch data from your existing WordPress site’s GraphQL endpoint. Do not deploy to the managed platform yet; just run next dev locally and iterate until the frontend matches your requirements.

Step 3: Implement Preview

Preview is the feature that will make or break editorial adoption. Implement the preview flow (following the platform-specific patterns described earlier) and test it thoroughly with your content team. If editors cannot preview their work, they will push back on the headless migration.

Step 4: Deploy to a Staging Environment

Deploy your frontend to the managed platform’s staging environment. On Atlas, this is a secondary environment. On VIP, it is the staging container. On Pantheon, it is the Dev or Test environment. Run the site in staging for at least two weeks with real editorial workflows.

Step 5: Set Up Cache Invalidation

Configure the cache invalidation strategy for your platform. On Atlas, this means enabling WPGraphQL smart cache. On VIP, it means configuring the caching headers and working with VIP’s team to tune the cache. On Pantheon, it means setting up Surrogate-Key-based purging.

// Example: On-demand revalidation API route (works on all platforms)
// pages/api/revalidate.js
export default async function handler(req, res) {
  // Verify the revalidation secret
  if (req.query.secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: "Invalid token" });
  }

  const { path } = req.query;

  if (!path) {
    return res.status(400).json({ message: "Missing path parameter" });
  }

  try {
    // Revalidate the specific path
    await res.revalidate(path);

    // Also revalidate the homepage and blog listing
    // since they likely display this content
    await res.revalidate("/");
    await res.revalidate("/blog");

    return res.json({
      revalidated: true,
      path: path,
      timestamp: Date.now(),
    });
  } catch (err) {
    console.error("Revalidation error:", err);
    return res.status(500).json({
      message: "Error revalidating",
      error: err.message,
    });
  }
}

Step 6: Go Live with a Parallel Run

Rather than a hard cutover, run both the traditional WordPress frontend and the new headless frontend in parallel. Use your DNS provider or CDN to split traffic: send 10% of traffic to the headless frontend and monitor performance, errors, and editorial workflows. Gradually increase the split to 25%, 50%, and finally 100% over several weeks.

This approach is possible on all three platforms because the WordPress backend remains the same. You are only changing where the frontend rendering happens.

Looking Ahead: The Managed Headless WordPress Space in 2022 and Beyond

The managed headless WordPress market is still young. Atlas launched in 2021, VIP’s decoupled offering has been evolving since 2020, and Pantheon Front-End Sites arrived in late 2021 / early 2022. All three platforms are actively developing features, and the gap between them will likely narrow over time.

Several trends will shape this space:

Edge rendering will become standard. As Next.js middleware and edge functions mature, all three platforms will likely move more rendering to the edge. Pantheon is ahead here with their edge-based architecture, but Atlas and VIP will follow.

WordPress full-site editing will challenge headless. WordPress’s ongoing investment in full-site editing and block themes makes traditional WordPress more capable. For some projects, the performance and flexibility advantages of headless will no longer justify the complexity cost once WordPress’s native rendering is fast and flexible enough.

Faust.js and similar frameworks will mature. Faust.js is still in its early versions. As it matures and adds App Router support, TypeScript improvements, and better block rendering, the case for custom Next.js setups may weaken. Similar frameworks may emerge from the Pantheon and VIP ecosystems.

Pricing will compress. As competition increases, pricing for managed headless WordPress will come down. Atlas already offers an accessible entry point; VIP may introduce more affordable tiers for smaller publishers; Pantheon will likely adjust their Front-End Sites pricing as the product matures.

For now, the choice between Atlas, VIP, and Pantheon comes down to your budget, your team’s technical comfort, and the specific requirements of your project. All three platforms deliver on the core promise of managed headless WordPress: you get a reliable deployment pipeline, a CDN, and a Node.js runtime without managing the infrastructure yourself. The differences are in the details, and those details matter most when you are debugging a production issue at 2 AM. Choose the platform whose details align with your team’s strengths.

Share this article

Priya Sharma

Frontend engineer specializing in Gutenberg block development and modern JavaScript in WordPress. Advocates for testing and code quality in the WordPress ecosystem.