{"record":{"id":"3cd6b80ee483ff15","repo":"puppetlabs/puppet","slug":"too-many-http-retries-for-addr","errorCode":null,"errorMessage":"Too many HTTP retries for %{addr}","messagePattern":"Too many HTTP retries for %(.+?)","errorType":"exception","errorClass":"Puppet::HTTP::TooManyRetryAfters","httpStatus":null,"severity":"error","filePath":"lib/puppet/http/retry_after_handler.rb","lineNumber":51,"sourceCode":"    else\n      false\n    end\n  end\n\n  # The amount of time to wait before attempting a retry\n  #\n  # @param [Net::HTTP] request\n  # @param [Puppet::HTTP::Response] response\n  # @param [Integer] retries number of retries attempted so far\n  #\n  # @return [Integer] the amount of time to wait\n  #\n  # @raise [Puppet::HTTP::TooManyRetryAfters] raise if we have hit our retry\n  #   limit\n  #\n  # @api private\n  def retry_after_interval(request, response, retries)\n    raise Puppet::HTTP::TooManyRetryAfters, request.uri if retries >= @retry_limit\n\n    retry_after = response['Retry-After']\n    return nil unless retry_after\n\n    seconds = parse_retry_after(retry_after)\n\n    # if retry-after is far in the future, we could end up sleeping repeatedly\n    # for 30 minutes, effectively waiting indefinitely, seems like we should wait\n    # in total for 30 minutes, in which case this upper limit needs to be enforced\n    # by the client.\n    [seconds, @max_sleep].min\n  end\n\n  private\n\n  def parse_retry_after(retry_after)\n    Integer(retry_after)\n  rescue TypeError, ArgumentError","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/http/retry_after_handler.rb#L33-L69","documentation":"When a server answers 429 or 503 with a Retry-After header, Puppet::HTTP::RetryAfterHandler sleeps for the indicated time (capped at Puppet[:runinterval]) and the client retries. Each attempt increments the count; when it reaches the client's retry_limit (default 100, set at Puppet::HTTP::Client.new), Puppet::HTTP::TooManyRetryAfters is raised with the request URI — the server never became healthy.","triggerScenarios":"A puppetserver/CA/LB persistently returning 503 (maintenance, overload, draining) or 429 rate-limiting across more than retry_limit consecutive retry cycles during one logical request.","commonSituations":"Puppet server restarting or upgrading while agents compile; rate limiters (Cloudflare, API gateways) throttling the agent; LB health checks flapping so backends keep toggling; runinterval short enough that the sleep cap makes retries cheap but the outage outlasts the budget.","solutions":["Check server health: the endpoint returning 503/429 is the root cause — resolve capacity or finish the maintenance window.","Inspect the Retry-After the server sends; if it is huge, waiting server-side beats burning the client retry budget.","If a transient storm is expected, construct Puppet::HTTP::Client.new(retry_limit: N) with a larger budget in that specific tool (not agents).","For rate limiting, stagger agent runs or spread load across server replicas."],"exampleFix":"# before\nclient = Puppet::HTTP::Client.new\n\n# after - tolerate a longer maintenance window in a maintenance script\nclient = Puppet::HTTP::Client.new(retry_limit: 200)","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  client.get(uri)\nrescue Puppet::HTTP::TooManyRetryAfters => e\n  sleep 300\n  raise if (attempts += 1) > 3\n  retry\nend","preventionTips":["Monitor servers returning 503/429 — the retry budget only masks the outage.","Set a realistic retry_limit per tool via Puppet::HTTP::Client.new(retry_limit: n).","Stagger agent runs and spread load so rate limits are not hit at all."],"tags":["puppet","http","retry","retry-after","service-unavailable"],"backgroundTag":"retry-limit-exceeded","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}