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

  1. Verify current tunnels with `cloudflared tunnel list` (deleted tunnels no longer appear as active).
  2. Treat this as success in idempotent cleanup scripts: the tunnel is already gone, so no action is needed.
  3. 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

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


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/40cb740d6514b98f. Report an issue: GitHub.