[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80515":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":16,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":20,"hasPages":20,"topics":22,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":16,"starSnapshotCount":16,"syncStatus":30,"lastSyncTime":31,"discoverSource":32},80515,"ncro","feel-co\u002Fncro","feel-co","Lightweight HTTP proxy for optimizing Nix cache routes for fast access","",null,"Rust",89,3,1,4,0,10,1.81,"European Union Public License 1.2",false,"main",[23,24,25,26],"nix","nix-binary-cache","nix-cache","nix-proxy","2026-06-12 02:04:03","\u003C!-- markdownlint-disable MD033 MD041 -->\n\n\u003Cdiv id=\"doc-begin\" align=\"center\">\n  \u003Ch1 id=\"header\">\n    \u003Cpre>ncro\u003C\u002Fpre>\n  \u003C\u002Fh1>\n  \u003Cp>\n    \u003Cb>N\u003C\u002Fb>ix \u003Cb>C\u003C\u002Fb>ache \u003Cb>R\u003C\u002Fb>oute \u003Cb>O\u003C\u002Fb>ptimizer\n  \u003C\u002Fp>\n  \u003Cbr\u002F>\n  \u003Ca href=\"#synopsis\">Synopsis\u003C\u002Fa>\u003Cbr\u002F>\n  \u003Ca href=\"#quick-start\">Quick Start\u003C\u002Fa> | \u003Ca href=\"#configuration\">configuration\u003C\u002Fa>\u003Cbr\u002F>\n  \u003Ca href=\"#hacking\">Contributing\u003C\u002Fa>\n  \u003Cbr\u002F>\n\u003C\u002Fdiv>\n\n## Synopsis\n\n`ncro` (pronounced Necro) is a lightweight HTTP proxy, inspired by Squid and\nseveral other projects in the same domain, optimized for Nix binary cache\nrouting. It routes narinfo requests to the fastest available upstream using EMA\nlatency tracking, persists routing decisions in SQLite and optionally gossips\nroutes to peer nodes over a mesh network. How cool is that!\n\n[ncps]: https:\u002F\u002Fgithub.com\u002Fkalbasit\u002Fncps\n\nUnlike [ncps], ncro **does not store NARs on disk**. It streams NAR data\ndirectly from upstreams with zero local storage. The tradeoff is simple:\nrepeated downloads of the same NAR always hit an upstream, but routing decisions\n(which upstream to use) are cached and reused. Though, this is _desirable_ for\nwhat ncro aims to be. The optimization goal is extremely domain-specific.\n\n### Motivation\n\nDuring a Nix build, binaries are downloaded from configured substituters, also\nknown as binary caches. When multiple caches serve the same paths or you have\nmultiple caches configured in your Nix setup, there is additional wait time and\noverhead to every build. ncro solves this by acting as an _intelligent local\nproxy_ that measures upstream latency in real time and routes each request to\nthe fastest responder. To keep ncro small and lightweight, routing metadata is\npersisted on disk; NAR content is streamed through with zero local storage. This\nkeeps the proxy stateless on the data path and eliminates cache-invalidation\ncomplexity.\n\n[architechture document]: .\u002Fdocs\u002Farchitecture.md\n\nFor a deeper look at the system design, see the [architechture document].\n\n### How It Works\n\n```mermaid\nflowchart TD\n    A[Nix client] --> B[ncro proxy :8080]\n\n    B --> C[\u002Fhash.narinfo request\u002F]\n    B --> D[\u002Fnar\u002F*.nar request\u002F]\n\n    C --> E[Parallel HEAD race]\n    E --> F[Fastest upstream wins]\n    F --> G[Result cached in SQLite TTL]\n\n    D --> H[Try upstreams in latency order]\n    H --> I{404?}\n    I -- yes --> J[Fallback to next upstream]\n    I -- no --> K[Zero copy stream to client]\n\n    J --> H\n    K --> A\n```\n\nThe request flow follows two distinct paths depending on the request type:\n\n### Narinfo Lookups\n\n1. Nix requests `\u002F\u003Chash>.narinfo`\n2. ncro checks the SQLite route cache; on a hit, it re-fetches from the cached\n   upstream without probing others\n3. On a miss, it races HEAD requests to all configured upstreams in parallel\n4. The fastest upstream wins; the full narinfo body is fetched from that\n   upstream and returned to the client\n5. The winning route is persisted with a configurable TTL; subsequent requests\n   for the same hash use the cached route directly\n\n### NAR Streaming\n\n1. Nix requests `\u002Fnar\u002F\u003Chash>.nar`\n2. ncro looks up the route for the corresponding narinfo hash; if no route is\n   found (e.g. the narinfo was requested directly from an upstream), it tries\n   upstreams in latency order\n3. The NAR body is streamed chunk-by-chunk from the selected upstream to the\n   client with zero buffering on disk\n4. If the upstream returns 404, ncro falls through to the next upstream in\n   latency order\n5. After all upstreams are exhausted with no success, a 404 is returned\n\nBackground probes (`HEAD \u002Fnix-cache-info`) run every 30 seconds to keep latency\nmeasurements current and detect unhealthy upstreams. System design is covered\nfurther in the [architechture document].\n\n### Runtime Endpoints\n\n- `GET \u002Fnix-cache-info`: proxy capability advertisement used by Nix\n- `GET \u002F\u003Chash>.narinfo`: route lookup and upstream selection\n- `GET \u002Fnar\u002F\u003Cpath>.nar`: streamed NAR content from the chosen upstream\n- `GET \u002Fmetrics`: Prometheus metrics\n- `GET \u002Fhealth`: JSON health summary of configured upstreams\n\n### Routing Notes\n\n- Route cache decisions are stored in SQLite and reused until their TTL expires\n  (or they are evicted by the LRU policy when `max_entries` is reached).\n- Latency is tracked using an Exponentially Weighted Moving Average (EMA) with a\n  configurable smoothing factor (`cache.latency_alpha`, default 0.3). Higher\n  alpha values react faster to changes; lower values filter out measurement\n  noise.\n- Lower latency wins the race. When two upstreams are within 10% of each other,\n  the lower `priority` value acts as a tiebreaker.\n- Background probes (`HEAD \u002Fnix-cache-info`) update latency estimates every 30\n  seconds even when no client traffic is flowing, ensuring warm routing data.\n- On a cache miss, ncro races all configured upstreams in parallel and returns\n  the first successful response. Unhealthy upstreams (detected by consecutive\n  probe failures) are excluded from the race until they recover.\n\n## Quick Start\n\n```bash\n# Run with defaults (upstreams: cache.nixos.org, listen: :8080)\n$ ncro\n\n# Point at a config file\n$ ncro --config \u002Fetc\u002Fncro\u002Fconfig.toml\n\n# Tell Nix to use it. The trusted key must match the upstream narinfo signer.\n$ nix-shell -p hello \\\n    --substituters http:\u002F\u002Flocalhost:8080 \\\n    --extra-trusted-public-keys cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\n```\n\n[installation document]: .\u002Fdocs\u002Finstall.md\n\nDeployment instructions are in [installation document].\n\n> [!TIP]\n> If you are testing locally, point only a single Nix client at ncro first. That\n> makes it easier to see cache behavior and upstream selection in logs.\n\n## Configuration\n\nDefault config is embedded; create a TOML file to override any field.\n\n```toml\n[server]\nlisten = \":8080\"\nread_timeout = \"30s\"\nwrite_timeout = \"30s\"\n\n[[upstreams]]\nurl = \"https:\u002F\u002Fcache.nixos.org\"\npriority = 10 # lower = preferred on latency ties (within 10%)\npublic_key = \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\"\n\n[[upstreams]]\nurl = \"https:\u002F\u002Fnix-community.cachix.org\"\npriority = 20\npublic_key = \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+\u002FrkCWyvRCYg3Fs=\"\n\n# S3-compatible cache (Garage, MinIO etc.)\n[[upstreams]]\nurl      = \"s3:\u002F\u002Fmy-bucket?endpoint=minio.example.com&scheme=https\"\npriority = 15\n\n# Private HTTP cache requiring Basic Auth\n[[upstreams]]\nurl      = \"https:\u002F\u002Fcache.internal.example.com\"\npriority = 5\nusername = \"ncro\"\npassword = \"hunter2\" # it says ******* on my screen it's secure!\n\n[cache]\ndb_path = \"\u002Fvar\u002Flib\u002Fncro\u002Froutes.db\"\nmax_entries = 100000 # LRU eviction above this\nttl = \"1h\"           # how long a routing decision is trusted\nnegative_ttl = \"10m\" # cache misses for a short window\nlatency_alpha = 0.3  # EMA smoothing factor (0 \u003C alpha \u003C 1)\n\n[cache.mass_query]\nmax_concurrent_races = 64     # total concurrent narinfo races\nper_upstream_max_inflight = 8 # per-upstream narinfo head concurrency\nin_memory_negative_ttl = \"5s\" # short-lived miss suppression\nupstream_cooldown = \"15s\"     # cooldown on transient upstream network errors\n\n[logging]\nlevel = \"info\" # debug | info | warn | error\nformat = \"json\" # json | text\n\n[discovery]\nenabled = false\nservice_name = \"_nix-serve._tcp\" # mDNS service type to browse\ndomain = \"local\"                 # mDNS domain\ndiscovery_time = \"5s\"            # how long to listen per discovery cycle\npriority = 20                    # priority assigned to discovered upstreams\naddress_family = \"any\"           # \"any\" | \"ipv4\" | \"ipv6\"\n\n[mesh]\nenabled = false\nbind_addr = \"0.0.0.0:7946\"\npeers = []       # list of {addr, public_key} peer entries\nprivate_key = \"\" # path to ed25519 key file; empty = ephemeral\ngossip_interval = \"30s\"\n```\n\n### Environment Overrides\n\n| Variable         | Config field    |\n| ---------------- | --------------- |\n| `NCRO_LISTEN`    | `server.listen` |\n| `NCRO_DB_PATH`   | `cache.db_path` |\n| `NCRO_LOG_LEVEL` | `logging.level` |\n\nEnvironment overrides are useful for containerized or Systemd deployments where\nyou want a fixed config file but still need to tweak one or two settings.\n\n### S3 Upstreams\n\nncro accepts Nix-style `s3:\u002F\u002F` URLs in the `url` field and fetches narinfo\u002FNAR\nobjects through the native AWS S3 SDK. Credentials are loaded through the\nstandard AWS provider chain: environment variables, shared config\u002Fcredentials\nfiles, `profile=`, or instance\u002Ftask identity where available.\n\nSupported query parameters:\n\n\u003C!--markdownlint-disable MD013-->\n\n| Parameter          | Description                                                                                                     |\n| ------------------ | --------------------------------------------------------------------------------------------------------------- |\n| `endpoint`         | Custom S3-compatible host (MinIO, Garage, Backblaze, ...).                                                      |\n| `scheme`           | `http` or `https`. Only meaningful with `endpoint`. Default: `https`.                                           |\n| `region`           | AWS region. Default: `us-east-1`.                                                                               |\n| `profile`          | AWS credential profile name for the standard AWS config\u002Fcredentials files.                                      |\n| `addressing-style` | `auto`, `path`, or `virtual`. Default: `auto`; custom endpoints and dotted bucket names use path-style in auto. |\n\n\u003C!--markdownlint-enable MD013-->\n\n```toml\n# S3-compatible store with a custom endpoint\n[[upstreams]]\nurl      = \"s3:\u002F\u002Fmy-bucket?endpoint=minio.example.com&scheme=https\"\npriority = 15\n\n# AWS S3 bucket with explicit region and credential profile\n[[upstreams]]\nurl      = \"s3:\u002F\u002Fmy-nix-cache?region=eu-west-1&profile=cache-readonly\"\npriority = 20\n```\n\n> [!NOTE]\n> `username`\u002F`password` are for HTTP Basic Auth upstreams only. S3 upstreams use\n> AWS credentials, for example `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`,\n> or a named `profile=`.\n\n### Upstream Authentication\n\nAny upstream can carry `username` and `password` fields. ncro itself sends HTTP\nBasic Auth on every request to that upstream: health probes, narinfo races, and\nNAR streaming.\n\n```toml\n[[upstreams]]\nurl      = \"https:\u002F\u002Fcache.internal.example.com\"\npriority = 5\nusername = \"ncro\"\npassword = \"hunter2\"\n```\n\n`password` is optional. Omit it for token-only schemes where the token goes in\nthe username field.\n\n## NixOS Integration\n\n```nix\n{lib, ...}: {\n  services.ncro = {\n    enable = true;\n    settings = {\n      upstreams = [\n        {\n          url = \"https:\u002F\u002Fcache.nixos.org\";\n          priority = 10;\n          public_key = \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\";\n        }\n\n        {\n          url = \"https:\u002F\u002Fnix-community.cachix.org\";\n          priority = 20;\n          public_key = \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+\u002FrkCWyvRCYg3Fs=\";\n        }\n      ];\n    };\n  };\n\n  # Point Nix at the proxy. By default the module appends every configured\n  # upstream public_key to nix.settings.trusted-public-keys; set\n  # services.ncro.addUpstreamPublicKeys = false to manage those keys yourself.\n  # NOTE: ncro needs to be the *only* substituter if you wish to benefit\n  # from its capabilities fully. If there are other substituters in your\n  # list, or if you don't mkForce this option, ncro will perform less\n  # efficiently.\n  nix.settings.substituters = lib.mkForce [ \"http:\u002F\u002Flocalhost:8080\" ];\n}\n```\n\nAlternatively, if you're not using NixOS, create a Systemd service similar to\nthis. You'll also want to harden this, but for the sake of brevity I will not\ncover that here. Make sure you have `ncro` in your `PATH`, and then write the\nSystemd service:\n\n```ini\n[Unit]\nDescription=Nix Cache Route Optimizer\n\n[Service]\nExecStart=ncro --config \u002Fetc\u002Fncro\u002Fconfig.toml\nDynamicUser=true\nStateDirectory=ncro\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n```\n\nPlace it in `\u002Fetc\u002Fsystemd\u002Fsystem\u002F` and enable the service with\n`systemctl enable`. In the case you want to test out first, run the binary with\na sample configuration instead.\n\n## Discovery Mode\n\nWhen `discovery.enabled = true`, ncro browses the local network for mDNS\nservices matching `service_name` (default `_nix-serve._tcp`) and registers each\ndiscovered instance as a dynamic upstream with `priority`.\n\nEvery routable address advertised by a discovered service is registered\nseparately. When `address_family = \"any\"` (default), both IPv4 and IPv6\naddresses are added so the router's race engine can try them in parallel. Set\n`address_family = \"ipv4\"` or `address_family = \"ipv6\"` to restrict to one\nfamily. This is _generally_ useful when your binary cache server only listens on\none stack (e.g. nix-serve binds `0.0.0.0` by default and does not accept IPv6\nconnections.)\n\nDiscovered upstreams are removed when they have not been seen for three\n`discovery_time` intervals.\n\n```toml\n[discovery]\nenabled = true\nservice_name = \"_nix-serve._tcp\"\ndomain = \"local\"\ndiscovery_time = \"5s\"\npriority = 20\naddress_family = \"ipv4\" # restrict to IPv4-only caches\n```\n\n## Mesh Mode\n\nWhen `mesh.enabled = true`, ncro creates an ed25519 identity, binds a UDP socket\non `bind_addr`, and gossips recent route decisions to configured peers on\n`gossip_interval`. Messages are signed with the node's ed25519 private key and\nserialized with msgpack. Received routes are merged into an in-memory store\nusing a lower-latency-wins \u002F newer-timestamp-on-tie conflict resolution policy.\n\nEach peer entry takes an address and an optional ed25519 public key. When a\npublic key is provided, incoming gossip packets are verified against it; packets\nfrom unlisted senders or with invalid signatures are silently dropped.\n\nIf `mesh.private_key` is left empty, ncro generates an ephemeral identity on\nstartup. That is fine for testing, but persistent gossip requires a stable key\nso peers can recognize the node across restarts.\n\n```toml\n[mesh]\nenabled = true\nprivate_key = \"\u002Fvar\u002Flib\u002Fncro\u002Fnode.key\"\n\n[[mesh.peers]]\naddr = \"100.64.1.2:7946\"\npublic_key = \"a1b2c3...\" # hex-encoded ed25519 public key (32 bytes)\n\n[[mesh.peers]]\naddr = \"100.64.1.3:7946\"\npublic_key = \"d4e5f6...\"\n```\n\nThe node logs its public key on startup (`mesh node identity` log line). You can\nshare it with peers so they can add it to their config.\n\n> [!TIP]\n> Keep mesh traffic on a private network. The gossip protocol is signed, but it\n> is still meant for trusted peers. ncro's mesh network feature was designed\n> with Tailscale in mind.\n\n## Metrics\n\nPrometheus metrics are available at `\u002Fmetrics`.\n\n\u003C!--markdownlint-disable MD013-->\n\n| Metric                                    | Type      | Description                              |\n| ----------------------------------------- | --------- | ---------------------------------------- |\n| `ncro_narinfo_cache_hits_total`           | counter   | Narinfo requests served from route cache |\n| `ncro_narinfo_cache_misses_total`         | counter   | Narinfo requests requiring upstream race |\n| `ncro_narinfo_requests_total{status}`     | counter   | Narinfo requests by status (200\u002Ferror)   |\n| `ncro_nar_requests_total`                 | counter   | NAR streaming requests                   |\n| `ncro_upstream_race_wins_total{upstream}` | counter   | Race wins per upstream                   |\n| `ncro_upstream_latency_seconds{upstream}` | histogram | Race latency per upstream                |\n| `ncro_route_entries`                      | gauge     | Current route entries in SQLite          |\n\n\u003C!--markdownlint-enable MD013-->\n\n> [!TIP]\n> If you are tuning upstreams, watch `ncro_upstream_latency_seconds` and\n> `ncro_upstream_race_wins_total` together. The first shows raw response timing;\n> the second shows which cache host is actually being chosen.\n\n## Operational Tips\n\n- Use `priority` to break ties between similarly fast caches, not to override a\n  clearly slower upstream.\n- Put `db_path` on persistent storage if you want routing decisions to survive\n  restarts.\n- Use a small `ttl` while testing and a larger one in production to reduce\n  upstream probing.\n- Keep `cache.nix.org` and any private caches in the upstream list, with the\n  most trusted cache first.\n- If you run behind a firewall or container network, make sure the listen port\n  is reachable from your Nix clients.\n\n## Hacking\n\nThis project is built with NixOS in mind and naturally the primary means of\nworking on this project is using Nix for a reproducible developer environment.\nUse `nix develop` to enter a development shell, or `direnv allow` to use the\nprovided `.envrc` if you use [Direnv](https:\u002F\u002Fdirenv.net).\n\n### Building\n\n```bash\n# With Nix (recommended)\n$ nix build\n\n# With Cargo directly\n$ cargo build --release\n\n# Development shell\n$ nix develop\n$ cargo test\n```\n\n## License\n\n\u003C!--markdownlint-disable MD033 MD059-->\n\n[provided here]: https:\u002F\u002Finteroperable-europe.ec.europa.eu\u002Fsites\u002Fdefault\u002Ffiles\u002Fcustom-page\u002Fattachment\u002Feupl_v1.2_en.pdf\n\nThis project is made available under European Union Public Licence (EUPL)\nversion 1.2. See [LICENSE](LICENSE) for more details on the exact conditions. An\nonline copy is [provided here].\n\n\u003Cdiv align=\"right\">\n  \u003Ca href=\"#doc-begin\">Back to the Top\u003C\u002Fa>\n  \u003Cbr\u002F>\n\u003C\u002Fdiv>\n\n\u003C!--markdownlint-enable MD033 MD059-->\n","ncro 是一个轻量级HTTP代理，专门用于优化Nix缓存路由以实现快速访问。它通过EMA延迟跟踪技术将narinfo请求路由到最快的上游，并使用SQLite持久化路由决策，同时可选地通过网格网络与对等节点共享路由信息。该项目采用Rust语言开发，具有高性能和低资源占用的特点。ncro特别适合在多Nix缓存配置环境下使用，能够显著减少构建过程中的等待时间和开销，提高整体效率。由于不存储NAR文件于本地磁盘上，而是直接从上游流式传输数据，因此非常适合需要高效缓存路由但不需要大量本地存储的场景。",2,"2026-06-01 03:51:24","CREATED_QUERY"]