tailscale/tailscale · warning
cert issuance for %v failed recently; next attempt no earlie
Error message
cert issuance for %v failed recently; next attempt no earlier than %v
What it means
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.
Source
Thrown at cmd/derper/ipcert.go:211
if ctx == nil {
ctx = context.Background()
}
return m.certForIP(ctx, connIP)
}
// certForIP returns the current certificate for ip, obtaining one
// first if there is no unexpired certificate for it. Concurrent
// callers for the same IP share a single issuance.
func (m *ipCertManager) certForIP(ctx context.Context, ip netip.Addr) (*tls.Certificate, error) {
m.mu.Lock()
e := m.entryLocked(ip)
if e.cert != nil && time.Now().Before(e.cert.Leaf.NotAfter) {
defer m.mu.Unlock()
return clipCert(e.cert), nil
}
if e.flight == nil && time.Now().Before(e.nextAttempt) {
m.mu.Unlock()
return nil, fmt.Errorf("cert issuance for %v failed recently; next attempt no earlier than %v", ip, e.nextAttempt.Format(time.RFC3339))
}
flight := m.startFlightLocked(ip, e)
m.mu.Unlock()
select {
case <-flight:
case <-ctx.Done():
return nil, ctx.Err()
}
m.mu.Lock()
defer m.mu.Unlock()
if e.cert == nil {
return nil, e.flightErr
}
return clipCert(e.cert), nil
}
View on GitHub (pinned to cfe32b8be6)
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.
Defensive patterns
Strategy: retry
Type guard
func isIssuanceBackoff(err error) bool {
return err != nil && strings.Contains(err.Error(), "failed recently; next attempt no earlier than")
} Try / catch
cert, err := mgr.CertForIP(ctx, ip)
if isIssuanceBackoff(err) {
// parse the RFC3339 deadline and re-enqueue after it; do NOT hammer
// fix the underlying issuance failure found in derper's logs first
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- new order: %w
- ACME account key: %w
- invalid PEM in %s
- cached cert is expired or not yet valid (NotAfter %v)
- register: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/c1f51686c0a5b8a8.
Report an issue: GitHub.