{"record":{"id":"40cb740d6514b98f","repo":"cloudflare/cloudflared","slug":"tunnel-s-has-already-been-deleted","errorCode":null,"errorMessage":"Tunnel %s has already been deleted","messagePattern":"Tunnel (.+?) has already been deleted","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"cmd/cloudflared/tunnel/subcommand_context.go","lineNumber":227,"sourceCode":"}\n\nfunc (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {\n\tforceFlagSet := sc.c.Bool(cfdflags.Force)\n\n\tclient, err := sc.client()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, id := range tunnelIDs {\n\t\ttunnel, err := client.GetTunnel(id)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"Can't get tunnel information. Please check tunnel id: %s\", id)\n\t\t}\n\n\t\t// Check if tunnel DeletedAt field has already been set\n\t\tif !tunnel.DeletedAt.IsZero() {\n\t\t\treturn fmt.Errorf(\"Tunnel %s has already been deleted\", tunnel.ID)\n\t\t}\n\n\t\tif err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil {\n\t\t\treturn errors.Wrapf(err, \"Error deleting tunnel %s\", tunnel.ID)\n\t\t}\n\n\t\tcredFinder := sc.credentialFinder(id)\n\t\tif tunnelCredentialsPath, err := credFinder.Path(); err == nil {\n\t\t\tif err = os.Remove(tunnelCredentialsPath); err != nil {\n\t\t\t\tsc.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)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// findCredentials will choose the right way to find the credentials file, find it,\n// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/cloudflare/cloudflared/blob/2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f/cmd/cloudflared/tunnel/subcommand_context.go#L209-L245","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\ncloudflared tunnel delete \"$TUNNEL_ID\"\n// after\nif cloudflared tunnel info \"$TUNNEL_ID\" 2>/dev/null; then\n  cloudflared tunnel delete \"$TUNNEL_ID\"\nfi","handlingStrategy":"validation","validationCode":"out, err := exec.Command(\"cloudflared\", \"tunnel\", \"info\", tunnelID).Output()\nif err != nil {\n    // tunnel likely gone; skip delete\n}","typeGuard":null,"tryCatchPattern":"// treat 'already been deleted' as success in cleanup scripts\nif strings.Contains(err.Error(), \"has already been deleted\") {\n    return nil\n}","preventionTips":["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."],"tags":["cli","api","idempotency","cloudflare"],"backgroundTag":"invalid-state-transition","analyzedSha":"2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f","analyzedAt":"2026-09-06T04:14:33.757Z","contentChangedAt":"2026-09-06T04:14:33.757Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}