cloudflare/cloudflared · warning
Tunnel %s has already been deleted
Error message
Tunnel %s has already been deleted
What it means
The `delete` command first fetches the tunnel record; if its DeletedAt timestamp is already set, the tunnel was deleted previously and cloudflared refuses to delete it again. This is a guard against redundant Cloudflare Tunnel API calls, not an API failure.
Source
Thrown at cmd/cloudflared/tunnel/subcommand_context.go:227
}
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
forceFlagSet := sc.c.Bool(cfdflags.Force)
client, err := sc.client()
if err != nil {
return err
}
for _, id := range tunnelIDs {
tunnel, err := client.GetTunnel(id)
if err != nil {
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", id)
}
// Check if tunnel DeletedAt field has already been set
if !tunnel.DeletedAt.IsZero() {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
}
if err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
}
credFinder := sc.credentialFinder(id)
if tunnelCredentialsPath, err := credFinder.Path(); err == nil {
if err = os.Remove(tunnelCredentialsPath); err != nil {
sc.log.Info().Msgf("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err)
}
}
}
return nil
}
// findCredentials will choose the right way to find the credentials file, find it,
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`View on GitHub (pinned to 2253eeeb25)
Solutions
- Verify current tunnels with `cloudflared tunnel list` (deleted tunnels no longer appear as active).
- Treat this as success in idempotent cleanup scripts: the tunnel is already gone, so no action is needed.
- Remove the tunnel's local credentials/config files if any remain, since the remote resource no longer exists.
Example fix
// before cloudflared tunnel delete "$TUNNEL_ID" // after if cloudflared tunnel info "$TUNNEL_ID" 2>/dev/null; then cloudflared tunnel delete "$TUNNEL_ID" fi
Defensive patterns
Strategy: validation
Validate before calling
out, err := exec.Command("cloudflared", "tunnel", "info", tunnelID).Output()
if err != nil {
// tunnel likely gone; skip delete
} Try / catch
// treat 'already been deleted' as success in cleanup scripts
if strings.Contains(err.Error(), "has already been deleted") {
return nil
} Prevention
- Make delete scripts idempotent: check tunnel existence before deleting.
- Avoid duplicate delete invocations in CI by marking the step done.
- Coordinate with teammates before deleting shared tunnels.
When it happens
Trigger: Running `cloudflared tunnel delete <name-or-id>` for a tunnel that was already deleted (e.g. the same command was run before, possibly with --force, or deleted via the dashboard/API).
Common situations: Rerunning a cleanup script after a partial failure; CI jobs that delete tunnels idempotently without checking; a teammate deleted the tunnel while your script still references it.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- failed to get checksums: {0}
- failed to upload checksum: {0}
- ErrUnauthorized
- ErrBadRequest
- ErrAPINoSuccess
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/40cb740d6514b98f.
Report an issue: GitHub.