# poetry install network error ci next to conda HTTP 000

> A poetry install network error ci failure and a conda HTTP 000 hide the same event. Two tools, two retry policies, two contradictory verdicts.

Source: https://latchkey.dev/learn/failures/poetry-and-conda-network-errors-in-ci  
Updated: 2026-09-21

A poetry install network error ci failure and its conda equivalent are the same event reported by two tools that disagree about what to do next, and reading either one at face value sends you in a direction the other would contradict. Poetry retries six times and then declines to choose between three possible causes; conda retries, gives up, and prints a placeholder status alongside advice to simply try again.

## What this error means

Poetry stops with `All attempts to connect to <host> failed.` and a Probable Causes block listing three possibilities that it has not narrowed down. Conda stops with `CondaHTTPError: HTTP 000 CONNECTION FAILED for url <...>`, an `Elapsed: -` line, and a paragraph telling you HTTP errors are often intermittent. Neither line contains a status code from a server, and in conda's case that is literal: the `000` and the words `CONNECTION FAILED` are the strings conda substitutes when the status code and the reason phrase are absent, which they are when no HTTP response ever arrived. Read as reported, both sound like the remote index is flaky. Read for what they can and cannot know, both are saying no conversation took place.

```Reconstructed from the PoetryRuntimeError raised in Authenticator._request, Poetry 2.5.1, with the Cleo style tags in the source removed as they are not printed
All attempts to connect to pypi.org failed.

Probable Causes
    - the server is not responding to requests at the moment
    - the hostname cannot be resolved by your DNS
    - your network is not connected to the internet
```

## Common causes

### DNS in the job is failing, and neither tool will say so

A resolver that is slow, rate limited or missing entirely produces a connection error that both tools bucket with everything else. Poetry lists DNS as one of three possibilities without deciding; conda reports no status because there was none. The way to settle it is from outside the tool, with a resolution check in the same step, because neither tool is going to distinguish it for you.

### Egress is blocked or proxied and the proxy is not configured

A runner that must go through an outbound proxy, with `HTTPS_PROXY` set for some steps and not others, fails at connect time for the steps that missed it. This produces the fast version of the poetry ladder, six attempts in a couple of seconds, because a refused connection does not have to time out. The elapsed time of the failing step is the clue.

### The index is up but slow, and the timeout is doing the failing

Poetry's 15 second per-request timeout is generous for metadata and tight for a large wheel over a congested link. When the timeout is what fires, the ladder takes about a minute and a half rather than a few seconds, and raising `POETRY_REQUESTS_TIMEOUT` genuinely helps. In our experience this is the only one of these four where a timeout change is the right fix.

### A private index or channel is rate limiting the matrix

Poetry retries 429 as part of its forcelist, so a throttled index produces a slow success or a slow failure rather than an immediate one. Conda surfaces a 4xx as its own error with the real status in the message, so the presence of a number other than 000 in a conda failure is itself informative: it means a server answered and you are reading its answer.

## How to fix it

### Prove whether it is name resolution, in the same step

1. Add a resolution and connection probe immediately before the install, aimed at the exact host the tool uses. It costs a second and it removes one of Poetry's three probable causes from the list.
2. Keep the probe in the workflow permanently. The value is in the failing run, and a probe added afterwards cannot tell you about the run that already failed.
3. Probe the private index too, not just the public one. A resolver that handles public names and not internal ones is a common split.

```.github/workflows/ci.yml
- name: Probe the index host
  run: |
    set -euo pipefail
    getent hosts pypi.org || echo "resolution failed"
    curl -sS -o /dev/null -w "connect=%{time_connect}s total=%{time_total}s code=%{http_code}\n" \
      --max-time 20 https://pypi.org/simple/
```

### Set the timeout deliberately rather than inheriting fifteen seconds

Poetry reads `POETRY_REQUESTS_TIMEOUT` at import time and defaults to 15. Raising it helps only when your failures are timeouts, and it makes every other kind of failure slower to report, so change it when the clock says the ladder is taking a minute and a half and leave it alone when the ladder finishes in seconds.

```.github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      POETRY_REQUESTS_TIMEOUT: "45"
    steps:
      - uses: actions/checkout@v5
      - run: poetry install --no-interaction --no-ansi
```

### Cache both tools so most runs make no requests at all

Poetry keeps its artifact cache under a directory you can key on `poetry.lock`, and conda keeps package tarballs under its own package directory. A restored cache removes the network from the equation for unchanged dependency sets, which is a stronger fix than any retry or timeout setting because it eliminates the request rather than making it more forgiving.

```.github/workflows/ci.yml
- uses: actions/cache@v4
  with:
    path: ~/.cache/pypoetry
    key: poetry-${{ runner.os }}-${{ hashFiles('poetry.lock') }}

- uses: actions/cache@v4
  with:
    path: ~/conda_pkgs_dir
    key: conda-${{ runner.os }}-${{ hashFiles('environment.yml') }}
```

### Read conda's status code before you act on its advice

A conda failure showing 000 means no response arrived, and a conda failure showing a real status means one did. The help paragraph is the same in both cases, so it cannot be used to tell them apart. Matching on the number is a one-line change that stops a DNS outage being treated as an intermittent server error.

```Terminal
grep -oE "HTTP [0-9]{3} [A-Z ]+ for (url|channel)" conda.log | sort | uniq -c
```

## How to prevent it

- Probe the index host in the same step that installs, so a failure carries its own DNS evidence.
- Cache the Poetry and conda package directories keyed on their lockfiles.
- Record the duration of the failing step. Poetry's ladder length tells you which kind of failure you had.
- Alert on conda's status number rather than on the word CondaHTTPError, because 000 and 503 need different owners.

## What each tool did before it spoke

Poetry's `Authenticator._request` runs a loop with `is_last_attempt = attempt >= 5`, so six attempts in total. It retries on `requests.exceptions.ConnectionError` and `OSError`, and on any response whose status is in `STATUS_FORCELIST`, which `poetry/utils/constants.py` defines as 429, 500, 501, 502, 503 and 504. The backoff is `0.5 * attempt`, so the pauses are half a second, one second, one and a half, two, and two and a half, unless the response carried a `retry-after` header, in which case that value is used as given. The per-request timeout is `REQUESTS_TIMEOUT`, read from `POETRY_REQUESTS_TIMEOUT` and defaulting to 15 seconds.

Add those up and the shape of a poetry failure becomes readable from the clock. Six attempts with under eight seconds of deliberate waiting between them means the whole ladder finishes quickly when connections are refused outright, and takes a minute and a half when each attempt has to time out. A step that failed in three seconds and a step that failed in ninety are two different diagnoses even though they print the same sentence.

Conda goes through urllib3 instead, and its retry activity is visible in the log as `Retrying (Retry(total=2, ...))` lines that name the connect timeout in force. When those are exhausted, `download.py` catches the `ConnectionError`, reads `status_code` off a response that does not exist, gets nothing, and hands that nothing to `CondaHTTPError`. The constructor turns it into a printable value with `status_code = status_code or "000"` and `reason = reason or "CONNECTION FAILED"`, and the same treatment gives you `Elapsed: -`.

|  | Poetry 2.5.1 | conda 24.1.2 |
| --- | --- | --- |
| Attempts | Six, in Poetry's own loop. | Four, through urllib3, visible in the log. |
| Backoff | Linear, 0.5 to 2.5 seconds, or `retry-after`. | urllib3 defaults, with the connect timeout printed. |
| Retries on status | 429, 500, 501, 502, 503, 504. | Transport failures. A 4xx is raised as its own error. |
| Per-request timeout | 15 seconds, via POETRY_REQUESTS_TIMEOUT. | Set by remote_connect_timeout_secs and its siblings. |
| What it says at the end | Three possible causes, undecided. | `HTTP 000 CONNECTION FAILED` and advice to retry. |

> The three Probable Causes lines are a fixed string in Poetry's source, not a diagnosis produced from the failure. They read as analysis and they are a constant, so treating them as a shortlist to work through is exactly right and treating them as evidence about your run is not.

## The conda block, and the sentence at the end of it

The block below really ran, on conda 24.1.2 on a laptop on 2026-09-21, against a channel URL pointed at the TEST-NET-1 address that nothing on the internet routes. The platform string in it is that machine's, which is one more reason to read it as what it says it is rather than as runner output.

The last paragraph is the part worth arguing with. It is a fixed help message that `download.py` attaches to every non-403 case, and it says HTTP errors are often intermittent and a simple retry will get you on your way. That is reasonable advice about a 503 and poor advice about the failure it is actually attached to here, where the `000` is conda telling you it never received a status at all. Four attempts had already been made and reported on the lines above it.

```Captured locally on conda 24.1.2, 2026-09-21, channel pointed at an unroutable address. Not runner output
CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://192.0.2.1/conda-forge/osx-arm64/repodata.json>
Elapsed: -

An HTTP error occurred when trying to retrieve this URL.
HTTP errors are often intermittent, and a simple retry will get you on your way.
```

## Why one block is a capture and the other is not

Conda was installed on the machine this page was written on and Poetry was not, and rather than make both blocks look alike, the labels say which is which. The conda block is quoted from a real process on a named version on a named day. The Poetry block is assembled from the exception raised in `Authenticator._request` at Poetry 2.5.1, with one change stated openly: the source wraps parts of that text in Cleo style tags, which the console formatter consumes rather than prints, so they are not in the block.

Neither is a Latchkey runner reproduction, and neither needs to be. The numbers that matter here are constants in the two tools, and constants are better read than measured. What a runner would contribute is a frequency, and a frequency claim on this page would need recorded aggregates rather than one job. `content/heal-evidence.mjs` has no record for this slug, so nothing here claims Latchkey repairs either failure.

## FAQ

### What does HTTP 000 CONNECTION FAILED mean in conda?

Both values are placeholders conda substitutes for missing information. In `CondaHTTPError`, the constructor does `status_code = status_code or "000"` and `reason = reason or "CONNECTION FAILED"`, and it is handed nothing for either because the underlying `requests.ConnectionError` carries no response object. So 000 is not a status any server sent, it is conda saying no response arrived. The matching `Elapsed: -` is the same substitution.

### How many times does poetry retry a failed request?

Six attempts in total. The loop in `Authenticator._request` sets `is_last_attempt = attempt >= 5` and increments from zero, retrying on connection errors and on the statuses in `STATUS_FORCELIST`, which are 429, 500, 501, 502, 503 and 504. Backoff is `0.5 * attempt` seconds, so under eight seconds of waiting across the whole ladder, unless a `retry-after` header overrides it.

### Should I trust conda when it says a simple retry will get me on my way?

Only when a real status accompanied it. That sentence is a fixed help message attached to every non-403 case in `download.py`, so it is printed whether the server returned 503 or nothing at all. Against a 000 it is advising you to retry something that already failed four times at the transport layer, which is why the number in front of it matters more than the paragraph.

### Does raising POETRY_REQUESTS_TIMEOUT fix "All attempts to connect failed"?

Only if your attempts were timing out. The default is 15 seconds per request, so six attempts that each time out take roughly a minute and a half, while six attempts against a refused connection take a couple of seconds. Compare the duration of the failed step against those two shapes first: raising the timeout on the fast version just makes the eventual failure slower.

## References

- [python-poetry/poetry: Authenticator._request, the retry loop and the final error](https://github.com/python-poetry/poetry/blob/2.5.1/src/poetry/utils/authenticator.py)
- [python-poetry/poetry: constants.py, STATUS_FORCELIST and REQUESTS_TIMEOUT](https://github.com/python-poetry/poetry/blob/2.5.1/src/poetry/utils/constants.py)
- [conda/conda: CondaHTTPError, where 000 and CONNECTION FAILED are substituted](https://github.com/conda/conda/blob/24.1.2/conda/exceptions.py)
- [conda/conda: download.py, which attaches the retry advice to every non-403 case](https://github.com/conda/conda/blob/24.1.2/conda/gateways/connection/download.py)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
