Front-End Web & Mobile

Amplify Hosting Announces Skew Protection Support

A common challenge in web application development and deployment is version skew between client and server resources. Today, we’re excited to announce deployment skew protection for applications deployed to AWS Amplify Hosting. This feature helps verifies end users have a seamless experience during application deployments.

The Challenge

Modern web applications are complex systems comprising numerous static assets and server-side components that must all work together. In a world where its common to have multiple deployments occur hourly, version compatibility becomes a critical concern. When new deployments occur, users with cached versions of your application may attempt to fetch resources from the updated deployment, potentially resulting in 404 errors and broken functionality.

This challenge is compounded by client-server version skew, which manifests in two common scenarios. First, users often keep browser tabs open for extended periods, continuing to run older versions of your application while attempting to interact with updated backend services. Second, in mobile applications, users with disabled auto-updates may continue using outdated versions indefinitely, requiring backend services to maintain compatibility with multiple client versions simultaneously.

These version management challenges can significantly impact user experience and application reliability if not properly addressed. Consider the scenario:

  1. A user loads your application (version A)
  2. You deploy a new version (version B)
  3. The user’s cached JavaScript tries to load assets that only existed in version A
  4. Result: broken functionality and poor user experience

How skew protection works

Amplify Hosting now intelligently coordinates request resolution across multiple client sessions, ensuring precise routing to intended deployment versions.

1. Smart asset routing

When a request comes in, Amplify Hosting now:

  • Identifies the deployment version that originated the request
  • Routes and resolves the request to the identified version of the asset
  • A hard refresh will always serve the latest deployment assets in a user session

2. Consistent version serving

The system confirms that:

  • All assets from a single user session come from the same deployment
  • New user sessions always get the latest version
  • Existing sessions continue working with their original version until refresh

Advantages to skew protection

Here are some of the advantages of having skew protection:

  • Zero Configuration: Works out of the box with popular frameworks
  • Reliable Deployments: Eliminate 404 errors due to deployment skew
  • Performance Optimized: Minimal impact on response times

Best practices

While skew protection handles most scenarios automatically, we recommend:

  1. Using atomic deployments
  2. Testing your deployment process in a staging environment

Enabling Skew Protection

Skew protection must be enabled for each Amplify app. This is done at the branch level for every Amplify Hosting app. You can read the full Amplify Hosting documentation here.

1. To enable a branch, click on App Settings, then click Branch Settings. Next, select the branch you want to enable followed by the Actions tab.

Screenshot of the branches tab in Amplify Console settings

FIGURE 1 – Amplify Hosting Branch Settings

 2. For skew protection to take effect, you must deploy the application once.

Note: Skew protection will not be available to customers using our legacy SSRv1/WEB_DYNAMIC applications.

Pricing: There is no additional cost for this feature and it is available to all Amplify Hosting regions.

Tutorial

To get started, follow these steps to create a Next.js application and enable skew protection on it.

Prerequisites

Before you begin, make sure you have the following installed:

  • Node.js (v18.x or later)
  • npm or npx (v10.x or later)
  • Git (v2.39.5 or later)

Create a Next.js app

Let’s create a Next.js app to see skew protection in action.

  1. Create a new Next.js 15 app with Typescript and Tailwind CSS
$ npx create-next-app@latest skew-protection-demo --typescript --tailwind --eslint
$ cd skew-protection-demo
Bash

2. Create a SkewProtectionDemo component that lists fingerprinted assets with deployment IDs. Use the following code to create the component.

Note: Amplify will automatically tag the fingerprinted assets of a NextJS app with a dpl query parameter set to a UUID. This UUID is also available during the build via the AWS_AMPLIFY_DEPLOYMENT_ID environment variable for other frameworks.

// app/components/SkewProtectionDemo.tsx

"use client";

import { useState, useEffect } from "react";
import DeploymentTester from "./DeploymentTester";

interface Asset {
  type: string;
  url: string;
  dpl: string;
}

export default function SkewProtectionDemo() {
  const [fingerprintedAssets, setFingerprintedAssets] = useState<Asset[]>([]);
  const [deploymentId, setDeploymentId] = useState<string>("Unknown");

  // Detect all assets with dpl parameters on initial load
  useEffect(() => {
    detectFingerprintedAssets();
  }, []);

  // Function to detect all assets with dpl parameters
  const detectFingerprintedAssets = () => {
    // Find all assets with dpl parameter (CSS and JS)
    const allElements = [
      ...Array.from(document.querySelectorAll('link[rel="stylesheet"]')),
      ...Array.from(document.querySelectorAll("script[src]"))
    ];
    
    const assets = allElements
      .map(element => {
        const url = element.getAttribute(element.tagName.toLowerCase() === "link" ? "href" : "src");
        if (!url || !url.includes("?dpl=")) return null;

        // Extract the dpl parameter
        let dplParam = "unknown";
        try {
          const urlObj = new URL(url, window.location.origin);
          dplParam = urlObj.searchParams.get("dpl") || "unknown";
        } catch (e) {
          console.error("Error parsing URL:", e);
        }

        return {
          type: element.tagName.toLowerCase() === "link" ? "css" : "js",
          url: url,
          dpl: dplParam,
        };
      })
      .filter(asset => asset !== null);

    setFingerprintedAssets(assets);
    
    // Set deployment ID if assets were found
    if (assets.length > 0) {
      setDeploymentId(assets[0]?.dpl || "Unknown");
    }
  };

  // Function to format URL to highlight the dpl parameter
  const formatUrl = (url: string) => {
    if (!url.includes("?dpl=")) return url;
    
    const [baseUrl, params] = url.split("?");
    const dplParam = params.split("&").find(p => p.startsWith("dpl="));
    
    if (!dplParam) return url;
    
    const otherParams = params.split("&").filter(p => !p.startsWith("dpl=")).join("&");
    
    return (
      <>
        <span className="text-gray-400">{baseUrl}?</span>
        {otherParams && <span className="text-gray-400">{otherParams}&</span>}
        <span className="text-yellow-300 font-bold">{dplParam}</span>
      </>
    );
  };

  return (
    <main className="min-h-screen p-6 bg-white">
      <div className="w-full max-w-2xl mx-auto">
        <h1 className="text-2xl font-bold text-gray-900 mb-6">
          Amplify Skew Protection Demo
        </h1>

        <div className="grid grid-cols-1 gap-4 mb-6">
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">🚀</span>
            </div>
            <div>
              <p className="font-medium">Zero-Downtime Deployments</p>
              <p className="text-xs text-gray-600">Assets and API routes remain accessible during deployments using deployment ID-based routing</p>
            </div>
          </div>
          
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">⚡️</span>
            </div>
            <div>
              <p className="font-medium">Built-in Next.js Support</p>
              <p className="text-xs text-gray-600">Automatic asset fingerprinting and deployment ID injection for Next.js applications</p>
            </div>
          </div>
          
          <div className="flex items-center p-4 bg-white text-gray-800 rounded-md border border-gray-200 hover:bg-gray-50 transition-colors">
            <div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg mr-3">
              <span className="text-lg">🔒</span>
            </div>
            <div>
              <p className="font-medium">Advanced Security</p>
              <p className="text-xs text-gray-600">Protect against compromised builds by calling the delete-job API to remove affected deployments</p>
            </div>
          </div>
        </div>

        <div className="bg-gradient-to-r from-blue-900 to-purple-900 p-4 rounded-md mb-6">
          <h2 className="text-sm font-medium text-blue-200 mb-1">
            Current Deployment ID
          </h2>
          <div className="p-2 bg-black bg-opacity-30 rounded-md font-mono text-lg text-center text-yellow-300">
            {deploymentId}
          </div>
        </div>

        {fingerprintedAssets.length > 0 ? (
          <>
            <h2 className="text-xl font-bold text-gray-900 mb-6">
              Fingerprinted Assets
            </h2>
            <div className="border border-gray-200 rounded-md overflow-hidden mb-6 bg-white">
              <div className="max-h-48 overflow-y-auto">
                <table className="min-w-full divide-y divide-gray-200">
                  <thead className="bg-gray-50">
                    <tr>
                      <th className="px-3 py-2 text-left text-xs font-medium text-gray-900 uppercase tracking-wider w-16">
                        Type
                      </th>
                      <th className="px-3 py-2 text-left text-xs font-medium text-gray-900 uppercase tracking-wider">
                        URL
                      </th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-gray-200">
                    {fingerprintedAssets.map((asset, index) => (
                      <tr key={index} className="bg-white hover:bg-gray-50">
                        <td className="px-3 py-2 text-sm text-gray-900">
                          <span
                            className={`inline-block px-2 py-0.5 rounded-full text-xs ${
                              asset.type === "css"
                                ? "bg-blue-100 text-blue-800"
                                : "bg-yellow-100 text-yellow-800"
                            }`}
                          >
                            {asset.type}
                          </span>
                        </td>
                        <td className="px-3 py-2 text-xs font-mono break-all text-gray-900">
                          {formatUrl(asset.url)}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
            
            <h2 className="text-xl font-bold text-gray-900 mb-6">
              Test Deployment Routing
            </h2>
            <DeploymentTester />
          </>
        ) : (
          <div className="p-6 text-center text-gray-600 border border-gray-200 rounded-md">
            <p>No fingerprinted assets detected</p>
            <p className="text-sm mt-2">
              Deploy to Amplify Hosting to see skew protection in action
            </p>
          </div>
        )}
      </div>
    </main>
  );
}
TypeScript

3. Next, create a DeploymentTester component that demonstrates how API requests maintain deployment consistency by sending the X-Amplify-Dpl header with each request, allowing Amplify to route to the correct API version. Use the following code to create the component.

// app/components/DeploymentTester.tsx

'use client';

import { useState } from 'react';

interface ApiResponse {
  message: string;
  timestamp: string;
  version: string;
  deploymentId: string;
}

export default function DeploymentTester() {
  const [testInProgress, setTestInProgress] = useState(false);
  const [testOutput, setTestOutput] = useState<ApiResponse | null>(null);
  const [callCount, setCallCount] = useState(0);

  const runApiTest = async () => {
    setTestInProgress(true);
    setCallCount(prev => prev + 1);
    
    try {
      const response = await fetch('/api/skew-protection', {
        headers: {
        // Amplify provides the deployment ID as an environment variable during build time
          'X-Amplify-Dpl': process.env.AWS_AMPLIFY_DEPLOYMENT_ID || '',
        }
      });
      
      if (!response.ok) {
        throw new Error(`API returned ${response.status}`);
      }
      const data = await response.json();
      setTestOutput(data);
    } catch (error) {
      console.error("API call failed", error);
      setTestOutput({
        message: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
        timestamp: new Date().toISOString(),
        version: 'error',
        deploymentId: 'error'
      });
    } finally {
      setTestInProgress(false);
    }
  };

  return (
    <div className="border border-gray-200 rounded-md overflow-hidden bg-white">
      <div className="bg-gray-50 px-4 py-3 flex justify-between items-center border-b border-gray-200">
        <span className="font-medium text-gray-800">Test Deployment Routing</span>
        <button
          onClick={runApiTest}
          disabled={testInProgress}
          className={`px-3 py-1 rounded text-sm ${
            testInProgress
              ? 'bg-gray-200 text-gray-500 cursor-not-allowed'
              : 'bg-blue-600 hover:bg-blue-700 text-white'
          }`}
        >
          {testInProgress ? "Testing..." : "Test Route"}
        </button>
      </div>
      
      <div className="p-4">
        {testOutput ? (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 font-mono text-sm">
            <div className="text-green-600 mb-2">{testOutput.message}</div>
            <div className="text-gray-600 text-xs space-y-1">
              <div>API Version: <span className="text-blue-600 font-medium">{testOutput.version}</span></div>
              <div>Deployment ID: <span className="text-purple-600 font-medium">{testOutput.deploymentId}</span></div>
              <div>Call #: {callCount}</div>
              <div>Time: {new Date(testOutput.timestamp).toLocaleTimeString()}</div>
            </div>
          </div>
        ) : testInProgress ? (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 text-sm text-gray-600">
            Testing deployment routing...
          </div>
        ) : (
          <div className="p-3 bg-gray-50 rounded border border-gray-200 text-sm text-gray-600">
            Click &quot;Test Route&quot; to verify how requests are routed to the correct deployment version
          </div>
        )}
      </div>
    </div>
  );
} 
TypeScript

4. Now create an API route that uses the X-Amplify-Dpl header to identify which deployment the request is coming from, simulating how Amplify routes API requests to maintain version consistency during deployments. Use the following code to create the API route:

// app/api/skew-protection/route.ts
import { NextResponse } from 'next/server';
import { type NextRequest } from 'next/server';

// This version identifier can be changed between deployments to demonstrate skew protection
const CURRENT_API_VERSION = "v2.0";

export async function GET(request: NextRequest) {
  // Get the deployment ID from the X-Amplify-Dpl header
  // This is how Amplify routes API requests to the correct deployment version
  const deploymentId = request.headers.get('x-amplify-dpl') || '';
  
  // Determine which version to serve based on deployment ID
  const apiVersion = CURRENT_API_VERSION;
  const message = `Hello from API ${apiVersion}! 🚀`;
  
  // Return the response with deployment information
  return NextResponse.json({
    message,
    version: apiVersion,
    deploymentId: deploymentId || 'none',
    timestamp: new Date().toISOString()
  });
} 
TypeScript

5. Add the Amplify deployment ID environment variable to make it accessible to client code

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  env: {
    AWS_AMPLIFY_DEPLOYMENT_ID: process.env.AWS_AMPLIFY_DEPLOYMENT_ID || '',
  }
};
export default nextConfig;
TypeScript

6. Push the changes to a GitHub repository

  • Create a new GitHub repository
  • Add and commit the changes to the Git branch
  • Add remote origin and push the changes upstream
git add .
git commit -m "initial commit"
git remote add origin https://github.com/<OWNER>/amplify-skew-protection-demo.git
git push -u origin main
Git

Deploy the application to Amplify Hosting

Use the following steps to deploy your newly constructed application to AWS Amplify Hosting:

  1. Sign in to the AWS Amplify console.
  2. Choose Create new app and select GitHub as the repository source
  3. Authorize Amplify to access your GitHub account
  4. Choose the repository and branch you created
  5. Review the App settings and then choose Next
  6. Review the overall settings and choose Save and deploy

Enable skew protection

In the Amplify console, navigate to App settings and then Branch settings. Select the Branch, and from the Actions dropdown menu choose Enable skew protection.

Screenshot of the branch settings in the AWS Console

FIGURE 2 – Amplify Hosting Branch Settings

Next, navigate to the deployments page and redeploy your application. When skew protection is enabled for an application, AWS Amplify must update its CDN cache configuration. Therefore, you should expect your first deployment after enabling skew protection to take up to ten minutes.

Screenshot of the AWS Amplify Console. Shows a deployed app

FIGURE 3 – Amplify Hosting App Deployments

Access the deployed Next.js app

Navigate to the Overview tab in the Amplify console and open the default Amplify generated URL in the browser. You should now observe a list of fingerprinted assets for your app along with the deployment ID.

Screenshot of Amplify Hosting settings

FIGURE 4 – Amplify Hosting App Settings – Branch level

Screenshot of the deployed app to see skew protection demo

FIGURE 5 – Demo App Homepage

Testing skew protection

When you deploy your Next.js application to Amplify, each deployment gets assigned a unique deployment ID. This ID is automatically injected into your static assets (JS, CSS) and API routes to ensure version consistency. Let’s see it in action:

  1. Asset Fingerprinting: Notice how each static asset URL includes a ?dpl= parameter with your current deployment ID. This ensures browsers always fetch the correct version of your assets.
  2. API Routing: The Test Route button demonstrates how Amplify routes API requests. When clicked, it makes a request to the /api/skew-protection endpoint. Since the request utilizes the X-Amplify-Dpl header to match your current deployment ID, it ensures routing to the correct API version.

This means that even during deployments, your users won’t experience version mismatches and each user’s session stays consistent with the version they initially loaded, preventing bugs that could occur when client and server versions don’t match.

Try it yourself

  1. Keep your current browser tab open and click Test Route to see the API version and deployment ID match.
  2. Deploy a new version with a different CURRENT_API_VERSION in api/skew-protection/route.ts
  3. Open your application in a new incognito window
    1. Compare the behavior:
        • Your original tab will maintain the old version
        • The new incognito window will show the new version
        • Each tab’s assets and API calls will consistently match their respective versions
        • Try clicking Test Route repeatedly in both windows – each will consistently route to its respective version, demonstrating how Amplify maintains session consistency even when multiple versions are live

      Screenshot of the demo app comparing both versions

FIGURE 6 -Side by Side Comparison of Skew Protection Behavior

This demonstrates how Amplify maintains version consistency for each user session, even when multiple versions of your application are running during deployments.

Congratulations, you’ve successfully created and verified skew protection on your Next.js application deployments on Amplify Hosting.

Cleanup

Delete the AWS Amplify app by navigating to App settings, next go to General settings, then choose Delete app.

Next Steps

  1. Enable Skew Protection for your application.
  2. Read our documentation to learn more about this feature!

About the Authors

Matt Headshot

Matt Auerbach, Senior Product Manager, Amplify Hosting

Matt Auerbach is a NYC-based Product Manager on the AWS Amplify Team. He educates developers regarding products and offerings, and acts as the primary point of contact for assistance and feedback. Matt is a mild-mannered programmer who enjoys using technology to solve problems and making people’s lives easier. B night, however…well he does pretty much the same thing. You can find Matt on X @mauerbac. He previously worked at Twitch, Optimizely and Twilio.

Jay Author

Jay Raval, Solutions Architect, Amplify Hosting

Jay Raval is a Solutions Architect on the AWS Amplify team. He’s passionate about solving complex customer problems in the front-end, web and mobile domain and addresses real-world architecture problems for development using front-end technologies and AWS. In his free time, he enjoys traveling and sports. You can find Jay on X @_Jay_Raval_