# bandersnatch: running your own PyPI mirror with the Simple API

> bandersnatch is the PyPI mirror client maintained under the Python Packaging Authority, implementing PEP 381, PEP 503 and PEP 691 so that a mirror speaks the same Simple API as the real index, including the JSON metadata format that has been the default since version 7.0. It runs on Python 3.12 or newer, ships a Docker image with a scheduled runner, and since 8.0 it has dropped xmlrpc and mirrors PEP 658 and PEP 714 core metadata files.

**pypa/bandersnatch** — A PyPI mirror client according to PEP 381 + PEP 503 + PEP 691

- Repository: https://github.com/pypa/bandersnatch
- Stars: 557 · Forks: 166
- Language: Python
- License: AFL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pypa-bandersnatch

## What bandersnatch mirrors and which specifications it implements

bandersnatch is a PyPI mirror client, and the specifications it implements are stated at the top of the project description: PEP 381 plus PEP 503 plus PEP 691. PEP 381 is the original proposal for a PyPI mirror, PEP 503 is the repository simple index that pip can be pointed at, and PEP 691 is the addition of JSON to the Simple API so that a client can ask for JSON metadata instead of parsing HTML.

That last one is the reason the tool has changed shape over the years. The documentation states that bandersnatch uses the PEP 691 Simple JSON API for package metadata, and that it has been the default since version 7.0. bandersnatch 6.0 is where the PEP 691 simple metadata mirroring was implemented, and version 8.0 is where xmlrpc support was removed, along with the arrival of PEP 658 and PEP 714 core metadata file mirroring through a `core-metadata` config option that is on by default.

The xmlrpc removal is worth pausing on, because xmlrpc was the mechanism PyPI used to announce new releases to mirrors, and its retirement is a sign of how the mirror ecosystem has moved. A modern mirror learns about new files by reading the index, and the core metadata files it now mirrors are the per-package metadata that PEP 658 added so that a client can resolve a dependency without downloading the whole distribution.

The project is supported and tested on Linux, Mac and Windows, and the maintainers ask for more help, pointing to a MAINTAINERS document that describes the roles and responsibilities along with a Mission Statement. The project is licensed under AFL-3.0, the last push was on 2026-10-19, and the recent releases are 8.0.0 from 2026-07-20, 8.1.0 from 2026-10-09 and 8.2.0 from 2026-10-14.

## Installing it three ways, and the Python version floor

There are three installation routes, and the first constraint applies to all of them: bandersnatch requires Python 3.12.0 or newer. The documentation is candid that it may work on older versions but that only 3.12 and up are tested, and that the code may use syntax from that minimum version.

The pip route installs the latest stable released version into a virtualenv, and the resulting executable lands at `bandersnatch/bin/bandersnatch`:

```shell
python3 -m venv bandersnatch
bandersnatch/bin/pip install bandersnatch
bandersnatch/bin/bandersnatch --help
```

The Docker route pulls the latest build, and the documentation advises using a specific tag if you want a pinned one. The image includes `/bandersnatch/src/runner.py` to periodically run a `bandersnatch mirror`, and you are pointed at the runner's own help output for usage:

```shell
docker pull pypa/bandersnatch
docker run pypa/bandersnatch bandersnatch --help
```

The image is a multi-stage build. The builder stage installs pip, setuptools and wheel into a target directory and then installs the dependency files into it, with a conditional branch on a `WITH_S3` build argument that adds `requirements_s3.txt` when the argument is set. The final stage is a python slim image with `LANG` and `LC_ALL` set to C.UTF-8, the dependencies and the package copied in, and a working directory of `/bandersnatch`. The command it starts is `python /bandersnatch/src/runner.py 3600`, which is the scheduled runner with a 3600 second interval, so the container mirrors on a timer rather than exiting.

The third route is a Docker Compose setup, which lives in its own directory in the source tree under `src/bandersnatch_docker_compose`, and there is also a `banderx` container in the tree that comes with an nginx configuration example for serving a mirror.

With Docker the recommendation is to bind mount a read only `bandersnatch.conf`, and the default path in the image is `/conf/bandersnatch.conf`.

## The quickstart, the configuration file and the webserver

The mirror workflow is deliberately simple. You run `bandersnatch mirror` and it creates an empty configuration file for you at `/etc/bandersnatch.conf`. You review that file and adapt it to your needs. You run `bandersnatch mirror` again and it populates your mirror with the current status of all PyPI packages, and the documentation points at the pypi.org stats page for the current mirror package size.

That first run is the one to plan around, because it is the whole of PyPI. A full sync is a large disk and bandwidth commitment, and the documentation says full syncs likely take hours depending on PyPI's performance and your network latency and bandwidth.

The way to shrink the mirror is filtering. A `blocklist` or `allowlist` can be created to cut down the mirror size, and the documentation suggests analysing PyPI downloads to work out which packages belong on your list rather than guessing.

After that you run `bandersnatch mirror` regularly to pick up intermediate changes, and the scheduling is a cron job. The sample entry for `/etc/cron.d/bandersnatch` is:

```cron
    LC_ALL=en_US.utf8
    */2 * * * * root bandersnatch mirror |& logger -t bandersnatch[mirror]
```

Two details in that line matter. The locale is set to en_US.utf8, which matters on a system where the default locale is not UTF-8, and the output is piped to `logger` with the tag bandersnatch[mirror], so the run appears in syslog. The documentation notes this assumes a `logger` utility is installed to convert the output into syslog entries, and it points at systemd timers as the modern alternative.

Serving the mirror is a webserver job rather than something bandersnatch does. You configure your webserver to serve the `web/` sub-directory of the mirror, and for PEP 691 support you need to respect the format the client requests. The documentation calls for publishing the HTML index files with UTF-8 as the charset, because the index pages work without it but look wrong to a human reader, and for the webserver to use UTF-8 to look up unicode path names. nginx gets this right by default, and the documentation says it is not sure about other web servers.

That last point is the one most likely to bite. A mirror with unicode path names served by a webserver that looks them up in a non-UTF-8 locale will 404 on files that exist.

## Maintenance, status files and the two commands marked dangerous

bandersnatch keeps little local state beyond the mirrored data itself. The documentation says that in general you can just keep rerunning `bandersnatch mirror` and it will fix errors, which is a pleasant property for a tool that runs unattended on a schedule.

When you want to be certain that everything matches the master index, there is a force option. Running `bandersnatch mirror --force-check` moves the status files if they exist in your mirror directory, which forces a full sync. The documentation warns that full syncs likely take hours, so this is a planned operation rather than a thing to try casually.

Two other commands carry explicit danger labels in the documentation. `bandersnatch delete --help` allows you to specify packages to be removed from your mirror, and it is marked dangerous. `bandersnatch verify --help` crawls your repository, fixes any missed files and deletes any unowned files found, and it is also marked dangerous. The second one is the one to think about, because it deletes files it does not recognise as belonging to the mirror, and a mirror directory that has been given other contents by hand is exactly the situation where that could remove something you wanted.

The operational requirements are also documented in detail, and they are the sort of thing that produces a broken mirror rather than a clear error.

You need a case-sensitive filesystem. macOS natively does this even though its filesystem is not strictly case-sensitive, so bandersnatch works fine there, but tarring a bandersnatch data directory and moving it to Linux with a case-sensitive filesystem leads to inconsistencies, and the documented fix is to delete the status files and let bandersnatch run a full check.

On Windows you need to run as Administrator, because bandersnatch makes use of symbolic links and that permission is off by default for non-admin users, so you either run it as Administrator or grant symlink permissions in the group policy editor.

And you need a filesystem that tolerates many sub-directories, because PyPI has an extensive list of packages to maintain in a flat directory. The documented failure is an OSError while syncing a package on a filesystem with a small limit on sub-directories per directory, and the example error in the documentation is from 2013, which tells you the limit has not gone away.

## Where bandersnatch is the wrong choice

The first limit is scale. A full mirror is all of PyPI, and the documentation points you at the pypi.org stats page for the current size, which is the number to look at before you commit disk. If your requirement is resilience rather than completeness, a caching proxy in front of pypi.org is a much smaller thing to run, and it gives you the same availability benefit without the storage. The filtering feature blurs this, because an allowlist of the fifty packages your builds actually use is a small mirror, but at that point you are running a private index rather than a mirror of PyPI, and other tools exist for that shape.

The second limit is the freshness model. A mirror is updated by a scheduled run, so there is a window where a newly published package or a yanked release has not propagated. The cron sample runs every two minutes, which is tight, but a full sync still takes hours and an interrupted sync leaves the mirror in a partial state that the next run repairs. For a team that installs packages by name from an empty environment on every CI run, that delay is a small annoyance. For a team that pins every dependency, it is irrelevant.

The third is the Python floor. Requiring 3.12 or newer is fine on a modern server and is a problem on a locked-down enterprise image with an old system Python. The documentation at least tells you the floor explicitly rather than leaving you to discover it.

The fourth is that bandersnatch is a client, not a server. It mirrors, it does not index, it does not resolve, and it has no notion of users, permissions or upload. If what you want is a private index where your own team publishes internal packages, that is a different piece of software entirely, and bandersnatch is the wrong half of the problem.

Compare that with the caching proxy approach, which stores what is requested rather than what exists. The proxy needs a fraction of the disk, starts serving immediately instead of after an hours-long first sync, and never has a stale package that upstream has already removed. The mirror trades that for completeness, offline availability and the guarantee that what you serve is exactly what PyPI had at a point in time, and for an air-gapped or restricted-network environment that guarantee is the reason to pick a mirror in the first place.

## Licence, development setup and what to check before the first sync

bandersnatch is licensed under AFL-3.0, with the LICENSE file in the repository root and the copyright held by the Python Packaging Authority. The Academic Free License is a permissive licence with an explicit patent grant and a termination clause for patent litigation, which for infrastructure software run by an institution is usually straightforward, but you should read the LICENSE file yourself rather than rely on the identifier.

The development setup is a modern Python project layout. The build backend is setuptools with a requirement of setuptools 83.0.0 or newer, and there is a setup.py that is a three-line shim delegating to setuptools. The runtime dependencies are pinned exactly in requirements.txt, and they tell you what the client does on the network: aiohttp 3.14.5 and aiohttp-socks 0.12.0 for asynchronous HTTP with proxy support, yarl and multidict and attrs as their transitive companions, idna for internationalised domain names, filelock for coordinating the mirror lock, humanfriendly for human readable sizes, and packaging for version handling. The optional requirements_s3.txt is the S3 backend, which is the branch the Dockerfile's WITH_S3 argument controls.

Testing is pytest based with a pytest.ini, a tox.ini, a coveragerc and a codecov configuration, and there is a CLAUDE.md and a test_runner.py at the top level. Formatting is black and linting is flake8 with a .flake8 configuration, and there is a pre-commit configuration. The repository also has a CHANGES.md and a MAINTAINERS.md.

What to check before the first sync is a short list. Confirm the filesystem is case-sensitive or, on macOS, plan to rebuild the status files if you ever move the data directory. On Windows, run as Administrator or set the symlink permission first. Make sure the filesystem tolerates a large number of sub-directories in the flat package directory, because that is where the OSError appears.

Then decide the scope before you run anything. Look at the mirror size on the pypi.org stats page, and either accept a full mirror or build the `allowlist` from your own download analysis, since the documentation points at the packaging guide for that. Put the cron entry in place from the start, and remember the `--force-check` option and the hours it takes.

## Conclusion

bandersnatch fits a company or institution that needs a stable, self-hosted package index for a build pipeline that cannot depend on pypi.org being reachable, and it fits anyone who needs to freeze or filter what their developers can install. It does not fit a small team that would be better served by a caching proxy, because a full mirror is a large disk commitment and a full sync takes hours. Before you start, put it on a case-sensitive filesystem, give it at least Python 3.12, and read the mirror size and cost implications in the pypi.org stats page before the first full run, then put a cron entry in place so the mirror is kept current instead of drifting.

## FAQ

### What is bandersnatch and which PEPs does it implement?

bandersnatch is a PyPI mirror client implementing PEP 381, PEP 503 and PEP 691. It uses the PEP 691 Simple JSON API for package metadata, which has been the default since version 7.0, and since 8.0 it has removed xmlrpc support and mirrors PEP 658 and PEP 714 core metadata files through the `core-metadata` config option.

### How do I install bandersnatch?

bandersnatch requires Python 3.12.0 or newer. You can install it into a virtualenv with `python3 -m venv bandersnatch` followed by `bandersnatch/bin/pip install bandersnatch`, or pull the Docker image with `docker pull pypa/bandersnatch`. The image includes `/bandersnatch/src/runner.py` to run `bandersnatch mirror` periodically, and with Docker the recommendation is to bind mount a read only bandersnatch.conf, defaulting to `/conf/bandersnatch.conf`.

### How do I start and maintain a PyPI mirror with bandersnatch?

Run `bandersnatch mirror` to create an empty configuration file at /etc/bandersnatch.conf, review and adapt it, then run the command again to populate the mirror. Run it regularly to pick up intermediate changes, with the documented cron sample in /etc/cron.d/bandersnatch running it every two minutes with output piped to logger, and serve the `web/` sub-directory of the mirror from your webserver.

### Can I make the mirror smaller than all of PyPI?

Yes. A `blocklist` or `allowlist` can be created to cut down the mirror size, and the documentation suggests analysing PyPI downloads to work out which packages to include. A first full sync populates the mirror with the current status of all PyPI packages and likely takes hours.

### What licence is bandersnatch released under?

bandersnatch is released under AFL-3.0, with the licence text in the repository root. The recent releases are 8.0.0 from 2026-07-20, 8.1.0 from 2026-10-09 and 8.2.0 from 2026-10-14, and the last push to the repository was on 2026-10-19.

## Sources

- [Issues](https://github.com/pypa/bandersnatch/issues)
- [License: AFL-3.0](https://github.com/pypa/bandersnatch/blob/main/LICENSE)
- [pypa/bandersnatch on GitHub](https://github.com/pypa/bandersnatch)
- [README](https://github.com/pypa/bandersnatch/blob/main/README.md)
- [Releases](https://github.com/pypa/bandersnatch/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pypa-bandersnatch
