{"record":{"id":"c1f51686c0a5b8a8","repo":"tailscale/tailscale","slug":"cert-issuance-for-v-failed-recently-next-attempt","errorCode":null,"errorMessage":"cert issuance for %v failed recently; next attempt no earlier than %v","messagePattern":"cert issuance for (.+?) failed recently; next attempt no earlier than (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"cmd/derper/ipcert.go","lineNumber":211,"sourceCode":"\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\treturn m.certForIP(ctx, connIP)\n}\n\n// certForIP returns the current certificate for ip, obtaining one\n// first if there is no unexpired certificate for it. Concurrent\n// callers for the same IP share a single issuance.\nfunc (m *ipCertManager) certForIP(ctx context.Context, ip netip.Addr) (*tls.Certificate, error) {\n\tm.mu.Lock()\n\te := m.entryLocked(ip)\n\tif e.cert != nil && time.Now().Before(e.cert.Leaf.NotAfter) {\n\t\tdefer m.mu.Unlock()\n\t\treturn clipCert(e.cert), nil\n\t}\n\tif e.flight == nil && time.Now().Before(e.nextAttempt) {\n\t\tm.mu.Unlock()\n\t\treturn nil, fmt.Errorf(\"cert issuance for %v failed recently; next attempt no earlier than %v\", ip, e.nextAttempt.Format(time.RFC3339))\n\t}\n\tflight := m.startFlightLocked(ip, e)\n\tm.mu.Unlock()\n\n\tselect {\n\tcase <-flight:\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tif e.cert == nil {\n\t\treturn nil, e.flightErr\n\t}\n\treturn clipCert(e.cert), nil\n}\n","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/cmd/derper/ipcert.go#L193-L229","documentation":"ipCertManager.certForIP has no unexpired certificate for the IP, no issuance flight in progress, and now is before the per-IP backoff deadline (e.nextAttempt) set when a previous issuance attempt failed. The request is rejected until the RFC3339 timestamp in the message, which rate-limits retry storms against the ACME CA.","triggerScenarios":"A prior obtainCert for this IP failed (port 80 unreachable, CA error, rate limit) and set nextAttempt; every new TLS handshake for that IP during the backoff window gets this error immediately without contacting the CA. Restarting derper clears the in-memory backoff.","commonSituations":"Firewall blocking HTTP-01 validation so issuance keeps failing; Let's Encrypt rate limits after repeated retries; operators restarting the daemon to 'fix' it and accidentally hammering the CA.","solutions":["Find the root cause first: check derper logs for the underlying issuance error from the failed attempt (the backoff message hides it).","Fix reachability of the HTTP-01 challenge (port 80 must serve the ipCertManager HTTPHandler) or the CA-side problem, then wait until the timestamp shown.","After fixing, restart derper to clear in-memory backoff and force a fresh attempt.","If rate-limited by the CA, wait out the CA's own window (see Let's Encrypt rate limit docs) before retrying."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"func isIssuanceBackoff(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"failed recently; next attempt no earlier than\")\n}","tryCatchPattern":"cert, err := mgr.CertForIP(ctx, ip)\nif isIssuanceBackoff(err) {\n    // parse the RFC3339 deadline and re-enqueue after it; do NOT hammer\n    // fix the underlying issuance failure found in derper's logs first\n}","preventionTips":["Treat the first issuance failure as the incident; backoff messages are symptoms.","Alert on underlying obtainCert errors, not just handshake failures.","Keep derper running so renewLoop refreshes certs before expiry instead of cold-starting into backoff.","Do not restart in a loop; restarts clear backoff and can trip CA rate limits."],"tags":["go","acme","rate-limit","backoff","derper"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}