Migrating from Cloudflare Pages to Workers (and cleaning up the mess)
Cloudflare is deprecating Pages in favor of Workers with Static Assets. It's not a sudden death, but the writing is on the wall. Time to migrate.
I had a setup with two separate projects: an 11ty static site on Pages and an API on Workers. Decided to merge them into one Worker project that handles both static assets and API routes. Clean and simple.
The migration itself was surprisingly smooth. Cloudflare's docs are solid, and there are some good community guides out there:
- Official Cloudflare migration guide
- Vibe Coding with Fred: Pages to Workers Migration
- Ben White: Pages to Workers
The new setup # Anchor link
Here's what a combined Worker + Static Assets config looks like:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-website",
"main": "worker/index.js",
"compatibility_date": "2025-11-17",
// Disable workers.dev subdomain (using custom domain only)
"workers_dev": false,
"preview_urls": false,
// Static assets configuration
// Static assets are served first EXCEPT for /api/* patterns
"assets": {
"directory": "./_site",
"binding": "ASSETS",
"not_found_handling": "404-page",
"run_worker_first": ["/api/*"]
},
// Custom domain routing
"routes": [
{
"pattern": "example.com",
"custom_domain": true
}
],
// Environment variables are stored in:
// - .dev.vars (local development, gitignored)
// - Cloudflare dashboard secrets (production)
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
// KV Namespaces
"kv_namespaces": [
{
"binding": "CACHE_KV",
"id": "your-kv-namespace-id"
}
],
// D1 Database
"d1_databases": [
{
"binding": "DB",
"database_name": "my_database",
"database_id": "your-d1-database-id"
}
]
}The key is run_worker_first — it routes API requests through the Worker while serving static files directly. Best of both worlds.
The unexpected problem # Anchor link
Everything worked great. But when I tried to delete the old Pages project... nope. 😤
"We are very sorry that you did not succeed."
— Cloudflare, when you try to delete a Pages project with 500+ deployments

This weekend I was playing the original King's Quest on my Steam Deck (bought the whole Sierra collection of 7 games on sale), and this game over message felt way too familiar. The game brings back warm memories of childhood when our whole family would sit together and play this wonderful adventure. If I remember correctly, it was even translated into Russian back in the day.
The dashboard just wouldn't let me. Too many deployments.
Turns out, Cloudflare has a "protection mechanism" to prevent accidental deletion of projects with a high number of deployments. Which is... fine, I guess? But also annoying when you actually want to delete something.
The official solution # Anchor link
Cloudflare documents this issue in their known issues page. Their solution? Download a Node.js script, run npm install, set environment variables, and execute it.
It works, but requires Node.js dependencies (node-fetch, exponential-backoff) and uses environment variables which end up in your shell history. I wanted something simpler.
I also found this StackOverflow question where others had the same problem.
My solution: a simple bash script # Anchor link
I wrote a quick bash helper that does the same thing. No npm, no extra dependencies. Just curl, jq, and a few minutes of your time.
#!/bin/bash
# Delete Cloudflare Pages/Workers deployments via API
# Usage: ./delete-cf-deployments.sh --project=xxx --account=xxx --token=xxx [--limit=N] [--keep=N]
PROJECT=""
ACCOUNT_ID=""
TOKEN=""
LIMIT=""
KEEP=""
for arg in "$@"; do
case $arg in
--project=*) PROJECT="${arg#*=}" ;;
--account=*) ACCOUNT_ID="${arg#*=}" ;;
--token=*) TOKEN="${arg#*=}" ;;
--limit=*) LIMIT="${arg#*=}" ;;
--keep=*) KEEP="${arg#*=}" ;;
esac
done
if [ -z "$PROJECT" ] || [ -z "$ACCOUNT_ID" ] || [ -z "$TOKEN" ]; then
echo "Usage: $0 --project=<name> --account=<id> --token=<api-token> [options]"
echo ""
echo "Required:"
echo " --project=NAME Cloudflare Pages project name"
echo " --account=ID Your Cloudflare account ID"
echo " --token=TOKEN Cloudflare API token"
echo ""
echo "Options:"
echo " --limit=N Delete only N deployments (for testing)"
echo " --keep=N Keep the last N deployments (cleanup mode)"
echo ""
echo "Examples:"
echo " $0 --project=my-site --account=abc123 --token=xyz789 --limit=3"
echo " $0 --project=my-site --account=abc123 --token=xyz789 --keep=10"
echo " $0 --project=my-site --account=abc123 --token=xyz789"
echo ""
echo "Get API token: Cloudflare Dashboard → My Profile → API Tokens"
echo " → Create Token → Edit Cloudflare Pages"
echo ""
echo "Get Account ID: Cloudflare Dashboard → any site → Overview"
echo " → right sidebar under 'API'"
exit 1
fi
API="https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/$PROJECT"
echo "Deleting deployments for: $PROJECT"
[ -n "$LIMIT" ] && echo "Limit: $LIMIT"
[ -n "$KEEP" ] && echo "Keeping last: $KEEP deployments"
echo ""
COUNT=0
PAGE=1
# For --keep mode, we need to skip the first N deployments
# API returns deployments sorted by date (newest first)
# So we paginate past the ones we want to keep
if [ -n "$KEEP" ]; then
# Calculate which page to start from
PER_PAGE=25
SKIP_PAGES=$((KEEP / PER_PAGE))
SKIP_IN_PAGE=$((KEEP % PER_PAGE))
PAGE=$((SKIP_PAGES + 1))
echo "Skipping first $KEEP deployments (starting from page $PAGE)..."
echo ""
fi
while true; do
# Fetch deployments with pagination
RESPONSE=$(curl -s "$API/deployments?per_page=25&page=$PAGE" \
-H "Authorization: Bearer $TOKEN")
IDS=$(echo "$RESPONSE" | jq -r '.result[].id // empty' 2>/dev/null)
if [ -z "$IDS" ]; then
echo "No more deployments."
break
fi
# Convert to array
IDS_ARRAY=($IDS)
START_INDEX=0
# On the first page in --keep mode, skip partial entries
if [ -n "$KEEP" ] && [ "$PAGE" -eq "$((SKIP_PAGES + 1))" ] && [ "$SKIP_IN_PAGE" -gt 0 ]; then
START_INDEX=$SKIP_IN_PAGE
echo "Skipping first $SKIP_IN_PAGE on this page..."
fi
for ((i=START_INDEX; i<${#IDS_ARRAY[@]}; i++)); do
ID="${IDS_ARRAY[$i]}"
echo "Deleting $ID..."
curl -s -X DELETE "$API/deployments/$ID?force=true" \
-H "Authorization: Bearer $TOKEN" > /dev/null
COUNT=$((COUNT + 1))
if [ -n "$LIMIT" ] && [ "$COUNT" -ge "$LIMIT" ]; then
echo ""
echo "Reached limit of $LIMIT. Deleted $COUNT deployments."
exit 0
fi
sleep 0.3
done
# In normal mode (no --keep), always fetch page 1 since we're deleting
# In --keep mode, move to next page
if [ -z "$KEEP" ]; then
PAGE=1
else
PAGE=$((PAGE + 1))
fi
done
echo ""
echo "Done! Deleted $COUNT deployments."
echo ""
echo "To delete the project entirely, run:"
echo " npx wrangler pages project delete $PROJECT"
echo ""
echo "Or delete it via Cloudflare Dashboard:"
echo " Workers & Pages → $PROJECT → Settings → Delete project"How it works # Anchor link
The script loops through all deployments using the Cloudflare API, deleting them one by one with a small delay to avoid rate limiting. Once done, it prints instructions for deleting the project via Wrangler or Dashboard.
Features # Anchor link
--limit=N- Test mode. Delete only N deployments to make sure everything works before going all in.--keep=N- Cleanup mode. Keep the last N deployments and delete the rest. Perfect for regular maintenance.- No dependencies - Just bash, curl, and jq. That's it.
Getting your credentials # Anchor link
You'll need two things:
API Token - Go to Cloudflare Dashboard → My Profile → API Tokens → Create Token → "Edit Cloudflare Pages" template.
Account ID - Open any site in Cloudflare Dashboard → Overview → look at the right sidebar under "API".
Example output # Anchor link
Deleting deployments for: my-old-project
Limit: 10
Deleting a1b2c3d4-e5f6-7890-abcd-ef1234567890...
Deleting b2c3d4e5-f6a7-8901-bcde-f12345678901...
Deleting c3d4e5f6-a7b8-9012-cdef-123456789012...
Deleting d4e5f6a7-b8c9-0123-defa-234567890123...
Deleting e5f6a7b8-c9d0-1234-efab-345678901234...
Deleting f6a7b8c9-d0e1-2345-fabc-456789012345...
Deleting a7b8c9d0-e1f2-3456-abcd-567890123456...
Deleting b8c9d0e1-f2a3-4567-bcde-678901234567...
Deleting c9d0e1f2-a3b4-5678-cdef-789012345678...
Deleting d0e1f2a3-b4c5-6789-defa-890123456789...
Reached limit of 10. Deleted 10 deployments.Get the script # Anchor link
I've put the script on GitHub Gist for easy access:
Feel free to use it, fork it, improve it. If you find any bugs or have suggestions, let me know!
Why not just use the official script? # Anchor link
The official solution works fine. But it requires Node.js, npm install, and passes credentials via environment variables (which end up in shell history). My bash script:
- Has zero dependencies beyond
curlandjq - Passes credentials as arguments (not in env vars)
- Adds
--keep=Noption to retain recent deployments - Works for both Pages and Workers projects
If you prefer Node.js, use theirs. If you want something lighter, use mine. 🤷
TL;DR: # Anchor link
Cloudflare won't let you delete Pages/Workers projects with too many deployments. Their solution needs Node.js. Mine is just bash. Grab the script and clean up your old deployments.
May the 4th be with you,
Alex