- Published on
Delete All Vercel Projects with a Node.js Script
- Authors

- Name
- Avasdream
- @avasdream_
Over time, Vercel accounts accumulate test projects, abandoned deployments, and experimental apps. Unfortunately, Vercel's dashboard only allows deleting projects one at a time. There's no native bulk delete feature.
This post provides a Node.js script that automates the process by leveraging Vercel's REST API.
Why Vercel Doesn't Have Bulk Delete
Vercel's REST API is designed around single-project operations. To delete multiple projects, you must:
- List all projects using paginated API calls
- Delete each project individually in a loop
Community threads frequently request a multi-select delete feature, but as of now, automation is the only solution.
API Endpoints Used
| Operation | Endpoint | Notes |
|---|---|---|
| List projects | GET /v10/projects | Returns paginated results with pagination.next cursor |
| Delete project | DELETE /v9/projects/{idOrName} | Use project id for safety |
Rate Limits
Vercel enforces rate limits on project operations. The delete endpoint allows approximately 100 requests per hour. The script handles 429 Too Many Requests responses by reading the retry-after header and waiting before retrying.
The Script
Create a file named delete-all-vercel-projects.mjs:
// delete-all-vercel-projects.mjs
const token = process.env.VERCEL_TOKEN;
if (!token) throw new Error("Set VERCEL_TOKEN");
const teamId = process.env.TEAM_ID; // optional
const base = "https://api.vercel.com";
async function api(path, { method = "GET" } = {}) {
const url = new URL(base + path);
if (teamId) url.searchParams.set("teamId", teamId);
const res = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}` },
});
// basic 429 handling
if (res.status === 429) {
const retryAfter = Number(res.headers.get("retry-after") || "5");
await new Promise(r => setTimeout(r, retryAfter * 1000));
return api(path, { method });
}
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`${method} ${url} -> ${res.status} ${text}`);
}
// DELETE returns 204 no content
return res.status === 204 ? null : res.json();
}
async function listAllProjects() {
const projects = [];
let from; // pagination cursor
while (true) {
const qs = new URLSearchParams();
qs.set("limit", "100");
if (from) qs.set("from", from);
const data = await api(`/v10/projects?${qs.toString()}`);
projects.push(...(data.projects || []));
from = data.pagination?.next;
if (!from) break;
}
return projects;
}
const projects = await listAllProjects();
console.log(`Found ${projects.length} projects. Deleting...`);
for (const p of projects) {
console.log(`Deleting ${p.name} (${p.id})`);
await api(`/v9/projects/${encodeURIComponent(p.id)}`, { method: "DELETE" });
// extra safety throttle (helps avoid rate limits)
await new Promise(r => setTimeout(r, 400));
}
console.log("Done.");
Code Walkthrough
Environment Variables
const token = process.env.VERCEL_TOKEN;
if (!token) throw new Error("Set VERCEL_TOKEN");
const teamId = process.env.TEAM_ID; // optional
The script requires a VERCEL_TOKEN for authentication. The TEAM_ID is optional—if omitted, the script operates on your personal account. If provided, it targets projects within that team scope.
API Helper Function
async function api(path, { method = "GET" } = {}) {
const url = new URL(base + path);
if (teamId) url.searchParams.set("teamId", teamId);
const res = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}` },
});
This helper constructs the full URL, appends the team ID if configured, and sends authenticated requests using the Bearer token.
Rate Limit Handling
if (res.status === 429) {
const retryAfter = Number(res.headers.get("retry-after") || "5");
await new Promise(r => setTimeout(r, retryAfter * 1000));
return api(path, { method });
}
When Vercel returns a 429 status, the script reads the retry-after header (defaulting to 5 seconds) and waits before recursively retrying the request.
Pagination
async function listAllProjects() {
const projects = [];
let from; // pagination cursor
while (true) {
const qs = new URLSearchParams();
qs.set("limit", "100");
if (from) qs.set("from", from);
const data = await api(`/v10/projects?${qs.toString()}`);
projects.push(...(data.projects || []));
from = data.pagination?.next;
if (!from) break;
}
return projects;
}
Vercel's API returns projects in pages of up to 100 items. The pagination.next field contains a cursor that must be passed as the from query parameter to fetch the next page. The loop continues until no more pages remain.
Deletion Loop
for (const p of projects) {
console.log(`Deleting ${p.name} (${p.id})`);
await api(`/v9/projects/${encodeURIComponent(p.id)}`, { method: "DELETE" });
// extra safety throttle (helps avoid rate limits)
await new Promise(r => setTimeout(r, 400));
}
Each project is deleted sequentially with a 400ms delay between requests. This throttling helps avoid hitting rate limits even when deleting many projects.
Usage
Get Your Vercel Token
- Go to Vercel Account Settings
- Create a new token with appropriate scope
- Copy the token value
Find Your Team ID (Optional)
If deleting team projects, find your team ID in the Vercel dashboard URL or team settings.
Run the Script
For personal account projects:
VERCEL_TOKEN="your_token_here" node delete-all-vercel-projects.mjs
For team projects:
VERCEL_TOKEN="your_token_here" TEAM_ID="team_xxx" node delete-all-vercel-projects.mjs
Adding a Dry Run Mode
Before deleting everything, you might want to preview what will be removed. Add this modification:
const dryRun = process.env.DRY_RUN === "true";
// ... in the deletion loop:
for (const p of projects) {
if (dryRun) {
console.log(`[DRY RUN] Would delete ${p.name} (${p.id})`);
} else {
console.log(`Deleting ${p.name} (${p.id})`);
await api(`/v9/projects/${encodeURIComponent(p.id)}`, { method: "DELETE" });
await new Promise(r => setTimeout(r, 400));
}
}
Run with:
VERCEL_TOKEN="xxx" DRY_RUN="true" node delete-all-vercel-projects.mjs
Filtering Projects
To delete only projects matching a specific prefix:
const prefix = process.env.PROJECT_PREFIX || "";
const filteredProjects = projects.filter(p =>
prefix ? p.name.startsWith(prefix) : true
);
console.log(`Found ${filteredProjects.length} matching projects. Deleting...`);
Important Considerations
| Consideration | Details |
|---|---|
| Irreversible | Deleted projects cannot be recovered. All deployments and configuration are permanently removed. |
| Scope matters | Without TEAM_ID, the script targets your personal account. With it, the team's projects are targeted. |
| Token permissions | Ensure your token has sufficient permissions to list and delete projects. |
| Rate limits | The 400ms throttle and 429 handling should prevent issues, but very large accounts may still hit limits. |