Estimating a GitHub repo's lines of code without downloading it
We cloned 250 popular repos created in the last 12 months, counted their code, and tested which cheap signal from GitHub's API predicts lines of code best. Repo size doesn't work well. GitHub's per-language byte counts do.
Key takeaways
- Use GitHub's language bytes, not repo size. Dividing the code bytes GitHub reports per language by 41 bytes per line lands within ±13% of the real count for the typical repo, and within 2× for 97% of repos.
- Repo size is off by about 2× for a typical repo, even with a separate ratio for every language and size bucket (median error 2.1×, only 47% within 2×).
- Bigger repos have far fewer lines per KB: 16.6 lines/KB for 100 KB–1 MB repos versus 1.7 for repos over 250 MB, a 9× drop. Large repos are mostly assets, data and model files, not code.
- Language matters less than expected. Code density ranges from 36 bytes per line (Go) to 46 (JavaScript), but the variation within a language and size cell is often 5–10×.
- Plan for the research: keep
size:as a loose search pre-filter, then make one language-bytes call per repo to estimate lines of code.
Why this matters
The upcoming research looks for new GitHub repos and filters or describes them by how much code they contain. GitHub search has no lines-of-code filter. Counting lines exactly means downloading each repo, which is fine for a shortlist but too slow for thousands of candidates. So we needed a cheap signal from the API that predicts lines of code, and to know how far off it can be.
There were two candidates:
- Repo size: the
sizefield on every repo, in KB, which can also be filtered in search withsize:[1]. It's free, but it measures the whole repository, not just code. - Language bytes: the languages endpoint returns the number of bytes of code in each language [2], computed by GitHub Linguist, which excludes vendored, generated and documentation files [3]. It costs one extra API call per repo.
Method
Sample
We picked 5 languages × 5 size buckets and took the 10 most-starred repos in each of the 25 cells. Every repo was created on or after 2025-11-02 and none are forks or archived. We used new repos because the research targets new repos, and old repos carry years of git history that inflates size. The language is GitHub's primary language for the repo.
Each number in the table below is a count of repos: how many public GitHub repos fall in that language and size range (and pass the filters above). That's the pool each cell's 10 repos were picked from. For example, there were 2.28 million Python repos of 100 KB–1 MB, and we cloned the 10 most-starred.
| Repos available | 100 KB–1 MB | 1–10 MB | 10–50 MB | 50–250 MB | > 250 MB | All sizes |
|---|---|---|---|---|---|---|
| Python | 2.28M | 1.51M | 728K | 350K | 85K | 4.96M |
| TypeScript | 3.20M | 1.75M | 746K | 296K | 55K | 6.05M |
| JavaScript | 2.24M | 1.71M | 787K | 286K | 54K | 5.08M |
| Go | 121K | 90K | 46K | 14K | 2K | 273K |
| Rust | 140K | 98K | 35K | 16K | 4K | 294K |
| All 5 languages | 7.98M | 5.16M | 2.34M | 962K | 202K | 16.65M |
Counting
- Each repo was cloned with only the latest commit (
git clone --depth 1), with Git LFS downloads skipped. The files were read, counted and deleted. Nothing in the repos was executed. - Lines of code means non-blank lines in source files (about 60 programming, markup and style languages, with comments included). Text formats such as Markdown, JSON, YAML, CSV and notebooks were counted separately and are not included.
- Files in vendor folders (
node_modules,vendor,third_partyand similar) and minified files (.min.*or averaging over 300 bytes per line) were also excluded. - 250 of 250 repos were usable. 2 had a few files that couldn't be written on Windows (partial checkout, most code still counted):
microsoft/waza,heygen-com/hyperframes.
Testing the estimators
Each estimator was scored leave-one-out: each repo is predicted using a ratio fitted on the other repos, so no repo helps predict itself. Error is the factor the estimate is off by, in either direction. "Off by 2×" means the estimate is half or double the real count.
Size vs lines of code
Both axes are logarithmic. If size predicted lines of code well, the points would form a tight line. Instead, at any given size the line count varies by one to two orders of magnitude. The dashed line is the best fit across all repos: LOC ≈ 58 × size_kb0.73. The exponent is below 1, so doubling a repo's size adds far less than double the code.
Lines per KB, by cell
Lines of code per KB of repo size, median, with the middle 50% (25th–75th percentile) below each value. Darker cells mean more code per KB.
| Language | 100 KB–1 MB | 1–10 MB | 10–50 MB | 50–250 MB | > 250 MB |
|---|---|---|---|---|---|
| Python | 3.31.7–7.4 | 5.14.2–6.6 | 6.52.7–9.1 | 2.91.8–3.3 | 1.20.0–2.6 |
| TypeScript | 26.318.8–35.9 | 5.93.2–17.9 | 5.85.2–11.6 | 4.42.9–9.1 | 2.91.4–5.4 |
| JavaScript | 16.73.4–23.9 | 4.03.4–5.3 | 2.01.1–4.1 | 2.20.6–4.0 | 0.60.3–0.8 |
| Go | 27.016.8–43.1 | 7.84.1–11.5 | 7.65.6–14.4 | 5.03.9–7.0 | 1.70.5–2.5 |
| Rust | 15.23.1–33.5 | 13.37.6–19.9 | 7.04.7–11.2 | 5.54.8–7.6 | 2.72.0–4.5 |
| All languages | 16.64.1–28.4 | 5.53.6–14.0 | 6.22.8–11.8 | 4.22.5–6.2 | 1.70.7–3.0 |
Two patterns stand out. Lines per KB falls steadily with size in every language. And the middle-50% ranges are wide: in many cells the 75th percentile is 3–10× the 25th, which is why a single ratio per cell can't give accurate estimates. Python's small repos are the exception to the trend: many of the top-starred ones are agent skills or small tools where most of the repo is prompts and docs, with only a few hundred to a few thousand lines of code.
Why size is a weak proxy
GitHub's size measures the whole repository as stored, including history and every file type. Code makes up a shrinking share of it as repos grow:
| Size bucket | Code share of size | Checkout ÷ size | Median LOC |
|---|---|---|---|
| 100 KB–1 MB | 60% | 1.51 | 4,410 |
| 1–10 MB | 25% | 1.10 | 31,478 |
| 10–50 MB | 25% | 0.88 | 193,057 |
| 50–250 MB | 17% | 0.59 | 474,650 |
| > 250 MB | 7% | 0.30 | 905,673 |
In the smallest bucket code is 60% of the size. Above 250 MB it is 7%, with the rest made up of images, videos, model weights, datasets, built bundles and history. The latest checkout is also often a different size from GitHub's figure, because size is the compressed repository including history. A new repo with a few large assets and a mature code-heavy repo can therefore be the same size.
Estimator comparison
| Estimator | Median error | 75th pct | 90th pct | Within ±25% | Within 2× |
|---|---|---|---|---|---|
| Size × one global ratio | 2.4× | 5.1× | 11.4× | 17% | 42% |
| Size × ratio per language | 2.4× | 4.7× | 10.2× | 17% | 40% |
| Size × ratio per size bucket | 2.1× | 4.0× | 10.2× | 19% | 48% |
| Size × ratio per language + bucket | 2.1× | 3.7× | 7.8× | 23% | 47% |
| Size, power-law fit | 2.2× | 4.2× | 8.9× | 10% | 43% |
| GitHub language bytes ÷ 41 B/line | 1.1× | 1.2× | 1.4× | 80% | 97% |
| GitHub language bytes ÷ per-language B/line | 1.1× | 1.2× | 1.4× | 83% | 96% |
Splitting the size ratio by size bucket helps (median error 2.4× → 2.1×). Splitting by language barely helps on top of that. The power-law fit does about as well as the per-bucket ratios with only two parameters. None of the size-based estimators gets close to the language-bytes approach.
The language-bytes estimator
"Code languages only" drops Linguist languages that aren't source code in our count: Jupyter Notebook, Markdown, TeX, Roff, Makefile, Dockerfile, Nix, HCL and template languages. 41 is the median bytes per non-blank line across all 250 repos.
Bytes per line by language
| Primary language | Median bytes / line | Middle 50% | Code in primary language | Median error (÷ 41) |
|---|---|---|---|---|
| Python | 45 | 42–48 | 79% | ±11% |
| TypeScript | 40 | 38–45 | 92% | ±11% |
| JavaScript | 46 | 42–52 | 88% | ±18% |
| Go | 36 | 34–39 | 87% | ±17% |
| Rust | 41 | 39–45 | 90% | ±5% |
The per-language divisors range from 36 to 46, but using them barely changes accuracy (see the estimator table). Repos mix languages (the primary language is typically 80–90% of the code), and the spread within a language is about as wide as the gap between languages, so one divisor of 41 is enough.
Biggest misses
| Repo | Actual LOC | Estimated | Est ÷ actual | Probable cause (inferred, not verified) |
|---|---|---|---|---|
| datawhalechina/easy-vibe | 241,938 | 5,165 | 0.02× | GitHub's language stats leave out Vue, which is most of the code here (stale stats or a .gitattributes override) |
| tamaratran/fast-jev-compaction | 13,125 | 1,554 | 0.12× | GitHub counts fewer bytes than are in the checkout: likely files marked vendored, generated or documentation by Linguist or the repo's .gitattributes |
| tt-a1i/archify | 1,021,492 | 211,646 | 0.21× | GitHub counts fewer bytes than are in the checkout: likely files marked vendored, generated or documentation by Linguist or the repo's .gitattributes |
| JimLiu/baoyu-skills | 203,051 | 47,496 | 0.23× | GitHub counts fewer bytes than are in the checkout: likely files marked vendored, generated or documentation by Linguist or the repo's .gitattributes |
| colbymchenry/codegraph | 1,320,827 | 529,912 | 0.40× | GitHub counts fewer bytes than are in the checkout: likely files marked vendored, generated or documentation by Linguist or the repo's .gitattributes |
| wheresryan22/anatomy | 52,613 | 25,036 | 0.48× | GitHub's language stats leave out HTML, which is most of the code here (stale stats or a .gitattributes override) |
| agentskills/agentskills | 1,485 | 714 | 0.48× | GitHub's language stats leave out JavaScript, which is most of the code here (stale stats or a .gitattributes override) |
| Leonxlnx/taste-skill | 4,942 | 10,273 | 2.08× | Code with long lines (data tables, inlined assets) inflates bytes relative to lines |
The large misses come from mismatches between what GitHub counts and what we counted: languages GitHub leaves out (stale stats or .gitattributes overrides), or bundled and minified files GitHub counts as code. These are rare, but they mean a single estimate can occasionally be far off.
How the research will use this
- Search with a loose
size:pre-filter to remove obvious mismatches without losing candidates. The table below shows where repos of each lines-of-code band actually fell in size (10th–90th percentile):
| Actual LOC band | Repos | Size p10 (KB) | Median (KB) | Size p90 (KB) |
|---|---|---|---|---|
| < 5k | 37 | 157 | 530 | 17,736 |
| 5k–50k | 67 | 296 | 2,713 | 57,815 |
| 50k–500k | 84 | 6,929 | 35,586 | 231,640 |
| > 500k | 62 | 89,059 | 302,232 | 1,028,821 |
The sample was stratified by size, so read this as a guide to how wide the pre-filter should be, not as population frequencies.
- Estimate lines of code for each candidate with one call to the languages endpoint ÷ 41. At 5,000 authenticated requests per hour [4], a few thousand repos fit comfortably. Search itself is limited to 30 requests per minute and 1,000 results per query [5], so broad searches need to be split into narrower ones.
- Count exactly only where it matters: repos near a cut-off, or a final shortlist, using the same clone-and-count script.
Limitations and open questions
- Popular, new repos only. The sample is the 10 most-starred repos in each cell, created in the last 12 months. Less popular or older repos may behave differently; older repos in particular carry more history in
size. - Five languages. Java, C/C++, C#, PHP and others weren't sampled. Their bytes per line are probably in the same range, but that's unverified.
- Lines of code is defined loosely. Non-blank lines include comments, and the vendored-folder and minified-file rules are heuristics. A tool like
clocorsccwould give somewhat different absolute numbers; the ratios should hold. - 10 repos per cell is small, so the per-cell ratios are noisy, especially in the largest bucket. The overall estimator comparison uses all 250 repos and is more robust.
- GitHub's language stats can lag or be overridden. They are refreshed on push and can be changed by the repo's
.gitattributes[3], which explains the worst misses.
All 250 repos
Click a column heading to sort. "Size est." is the per-language + bucket estimate (leave-one-out); "Bytes est." is language bytes ÷ 41.
| Repo | Language | Size bucket | Stars | Size (KB) | LOC | LOC/KB | Size est. | Bytes est. | Main languages counted |
|---|
Sources
- Searching for repositories, GitHub Docs.
size:qualifier in kilobytes. Accessed 2026-11-02. - REST API endpoints for repositories: List repository languages, GitHub Docs. "Number of bytes of code written in that language." Accessed 2026-11-02.
- Linguist: Overrides, GitHub Linguist. Vendored, generated and documentation files are excluded from language stats, and can be overridden in
.gitattributes. Accessed 2026-11-02. - Rate limits for the REST API, GitHub Docs. 5,000 requests per hour for authenticated users. Accessed 2026-11-02.
- REST API endpoints for search, GitHub Docs. Repository search 30 requests per minute; up to 1,000 results per search. Accessed 2026-11-02.
- Primary data: this study's clone-and-count run over 250 repos, 2026-11-02.