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

  1. Find the root cause first: check derper logs for the underlying issuance error from the failed attempt (the backoff message hides it).
  2. 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.
  3. After fixing, restart derper to clear in-memory backoff and force a fresh attempt.
  4. 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

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


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/c1f51686c0a5b8a8. Report an issue: GitHub.