Do crawlers react to new website commits?

Do crawlers react to new website commits?

Commit-response study of the public Forgejo repository neuwirth/levineuwirth.org, October 1 to October 19, 2026 (UTC). Prepared 2026-10-20. All times are UTC.

Short answer

We found no convincing evidence of repeated, prompt fetching after pushes. The dominant user-agent cohort mostly revisited older commits and made no commit-specific GET within an hour of publication. That argues against a crawler that promptly fetched every new commit. It leaves intermittent or otherwise unobserved monitoring unresolved.

The planned timing test is null. The more direct evidence is what was fetched, and when.

Figures: timeline, episodes and null, leads and lags, what was fetched, time to first fetch. Each also exists as SVG.

1. Question and design status

The author asked whether a scraper was “monitoring for new commits and specifically firing on each of them”.

Labels below follow this division. Raw logs, addresses, user agents, queries and request sequences stayed on the VPS. Only aggregates are here.

2. Publication: what became visible, and when

Source: data/publication-snapshot.json, a fresh read-only collection at 2026-10-19 22:23:19Z. It holds all 345 commit objects in the store, reflogs, refs, Forgejo action and branch rows, and no messages, names or emails. Reconstruction: source/reconstruct_publication.py → data/publication-episodes.csv, data/commit-publication.csv and data/publication-audit.json.

History rewrite, and no left censoring. The current graph has a single root commit, committed 2026-08-29 15:25:51Z.

Units. A publication episode is the set of ref transactions in the same second.

Item Finding
Reflog vs notifications Every retained branch update (82 since Aug 29; 49 in October) matches a notification within 30 s; lag 0–20 s (median 2 s); no reflog update lacks one
Oct 1 12:27:01 One episode: main plus two newly created branches, 14 novel commits
upgrade-check Created, pushed and deleted within one notification second, 2026-10-01 19:49:36; transaction in [19:49:16, 19:49:36], minute certain
upgrade-check content Notification Len 10 is a recent-commit cap. It exposed 2 novel commits, which main later carried at 21:11:07 (that push’s Len 7 includes them; novel count 5)
upgrade-check context Post-upgrade verification push after the Forgejo 1.21 → 15.0.9 chain (19:24–19:45)
Tags, releases, mirrors None for this repository
Commits never published None
GitHub precedence origin pushes to Forgejo then GitHub; GitHub events trail Forgejo by 2–4 s for every Forgejo push
GitHub-only pushes Nine GitHub-only pushes (Oct 6 17:19 to Oct 10 10:24) put the novel commits of 7 Forgejo episodes (E071–E076, E080) on GitHub 4.5 min to 17.6 h before Forgejo
Requests before publication No request named a current commit before its first Forgejo publication. No reconstruction error, no earlier visibility

Commit-creation times were never used as publication times.

3. Measurement, coverage and verification

Aggregation. source/aggregate_requests.py ran read-only on the VPS over the private archive, 2026-09-27 00:00 to 2026-10-19 22:00. That covers 16 archives: 2,135,701 legacy lines and 1,384,453 forge JSON lines.

source/build_tables.py produced the following:

Verification. Hourly requests, HTTP 200s and HTTP 403s were rebuilt with the existing parser’s definitions (scope = repository, all methods, checks excluded). For all 312 hours of Oct 1–13 they match 2026-10-14/hourly-summary.csv exactly, for all agents, the Linux/Chrome cohort and the Meta cohort. A re-run of the aggregator, after refactoring the owner pass, reproduced every series and hash row exactly.

Coverage (data/coverage.json):

None of these touches a push window except the upgrade and the archive switch, which exclude E039 and E040. E057 crosses the log cutover.

Owner and internal traffic. Six owner addresses were identified privately, from successful pushes and authenticated pages.

What the cohort looked like. The linux-chrome-150-155 cohort is a user-agent heuristic (X11 Linux x86_64 with Chrome 150 or 155), not a verified identity.

4. Planned primary test

Hypotheses.

Statistic and null.

Why the p-value is valid. Block rotations form a group, so this p-value is valid under H0 (Hemerik & Goeman 2018; Phipson & Smyth 2010).

What the test does not establish.

Eligibility (A6).

Result.

Quantity Value
Episodes / hour blocks / overlap clusters 40 / 37 / 32
Raw T (mean post − pre) 4.25 requests per episode (829 after vs 659 before)
Null mean (exact expected raw difference over rotations) 1.89
Rotation-adjusted excess (T − null mean) +2.36
95% interval for the adjusted excess, overlapping-window clusters (32) −1.47 to +7.75
95% interval for the adjusted excess, whole UTC dates (10) −1.97 to +10.47 (few clusters; not a validated guarantee)
One-sided p 0.151
Episodes with zero in both windows 28 of 40 (the cohort was absent before Oct 6)
Plan-literal, each ref update a batch T = 4.10, p = 0.146 (42 batches)
Leave one date out p from 0.072 (without Oct 10) to 0.712 (without Oct 13: raw +0.55, adjusted −0.49)
Cohort-active dates only (Oct 6–13; post-review) 15 episodes, 6 dates: raw +11.2, adjusted +6.27, p = 0.153

Oct 13 is the only influential day (figure). Its two episodes (E081, E082, one commit each) sit inside the Oct 13 daily ramp.

5. Planned sensitivities and amended analyses

Planned sensitivity rows are as recorded. Amended rows are A5–A11. All tests are one-sided against an increase. Only the window family is multiplicity-adjusted. Values are raw T, with the rotation-adjusted excess in parentheses.

Analysis Episodes Raw T (adjusted excess) p
Planned: isolated (no other episode within 60 min) 26 6.31 (3.49) 0.158
Planned: all user agents, 200 40 −16.25 (−23.06) 0.769
Planned: cohort, all statuses (GET) 40 9.50 (5.29) 0.115
Planned: other repositories, cohort 200 (negative control) 40 −2.85 (0.24) 0.448
Planned: phase before Anubis, cohort 20 0.10 (2 vs 4 requests in total) 0.485 (no data)
Planned: phase before Anubis, all agents 20 −41.2 (−51.1) 0.796
Planned: phase difficulty 2, cohort 20 8.40 (4.71) 0.151
Planned: window 5 min 41 0.32 (0.27) 0.238 (Holm 0.476)
Planned: window 60 min 39 10.97 (4.88) 0.110 (Holm 0.329)
Planned: window 180 min 37 35.27 (1.91) 0.337 (Holm 0.476)
Amended: owner and internal addresses removed 40 identical to primary 0.151
Amended: GitHub-preceded episodes removed 33 4.85 (3.97) 0.048 nominal; without Oct 13, 0.36 (0.58), p 0.37
Amended: start time instead of completion (JSON era) 20 8.45 (4.76) 0.148
Amended: blocks offset to :30 39 4.36 (0.91) 0.287
Amended: relaxed log-regime rule 40 unchanged (E057’s cutover minute is uncovered) 0.151
Amended: share of episodes with post > pre 40 0.55 (+0.02) 0.389
Amended: mean log(1 + post) − log(1 + pre) 40 0.021 (−0.010) 0.582
Amended: batch-size-weighted episode mean, Σ nᵢDᵢ / Σ nᵢ (previously mislabeled “per novel commit”) 40 0.77 (−0.05) 0.545
Amended (post-review): requests per novel commit, Σ Dᵢ / Σ nᵢ over 235 commits 40 0.72 (0.40) 0.151 (a rescaling of the primary statistic, not an independent result)
Amended: time-of-day referents (same clock minutes on other eligible days of the phase) 40 4.25 (3.21) 0.141
Amended: time-of-day referents, all agents 40 −16.25 (−13.33) 0.887

The single nominal p below 0.05 (GitHub-preceded removed) is one of about 20 tests in the table. It disappears without Oct 13.

Exploratory endpoint and route splits (results/timing-results.json, exploratory; same design, unadjusted, not confirmatory):

6. What was fetched

source/hash_analysis.py covers GETs naming a commit in a semantic path position (src/commit/<sha>, commit/<sha>[.diff|.patch], commits|raw|media|blame|rss/commit/<sha>, archive/<sha>.*, compare/<a>...<b> and the repository API).

Cohort linux-chrome-150-155 All agents
GETs naming a current commit 90,122 206,993
…target under 1 h old 0 —
…1–6 h / 6–24 h / 1–3 d / 3–7 d / over 7 d 36 / 568 / 57 / 2,191 / 87,270 —
Requests per available commit-hour, under 1 h → over 7 d 0 · 0.07 · 0.28 · 0.01 · 0.12 · 2.00 —
Share of 271 study commits fetched by 1 h (Kaplan–Meier) 0% 5.2%
…by 6 h / 24 h / 7 d 0.7% / 1.5% / 13.7% 7.8% / 22.9% / 51.3%
Commits ever fetched by Oct 19 22:00 120 140
Study episodes with any fetch within 1 h 0 of 48 4 of 48 (E035, E053, E058, E063); 3 of 43 without the Oct 4 deployment day
Youngest target, age at the start of the request minute 1.35 h —

Ages use the request-minute midpoint (completion times, minute resolution). At the start of the request minute, 87,265 of the cohort’s requests are definitely older than 7 days (87,270 by midpoint).

The scatter of request time against publication time is the clearest view.

Per-episode first-fetch lags are in results/first-fetch-by-episode.csv.

How the tips were found. The cohort requested branch views (src/branch, commits/branch and raw/branch for main, which display the current tip) 218 times on Oct 9. On every other day it made 0 to 6 such requests.

This timing is consistent with link traversal that happened to pass through the branch page. It is hard to reconcile with a monitor that fetches each push’s commits promptly. The logs cannot identify the mechanism uniquely, and polling that did not name commits would leave no trace in this analysis.

7. Sources of the fast fetches (private analysis, aggregates only)

Source: source/private_sources.py, run by source/run_private_sources.sh. Output reduced by source/summarize_private_sources.py to results/young-fetch-sources.json. “Sources” are addresses; rotating addresses need not be distinct operators.

Within 60 minutes of publication. 107 GETs from 10 addresses, in 4 episodes; none had an owner signal.

The cohort’s young fetches (E076–E079). 604 requests from 265 addresses.

8. Alternative explanations checked

9. Power, robustness and what the null can say

source/power.py: placebo push trains are drawn from the rotation null, a response is planted into the real cohort series in minutes +1…+10, and the planned test is re-run 300 times per size.

What the simulation measures. It is conditional on its injection model.

Injection model (300 simulations each) Detections at α = 0.05
none (placebo false-positive rate) 0.067
all 40 eligible pushes: +2 / +5 / +10 / +20 requests 0.24 / 0.64 / 1.00 (300/300) / 1.00
all 40, expected +0.5 / +1 / +2 per novel commit 0.36 / 0.71 / 0.99
difficulty-2 pushes only (20): +5 / +10 / +20 0.37 / 0.68 / 1.00
cohort-active dates only (Oct 6–13; 15 pushes, 6 dates): +5 / +10 / +20 / +40 0.23 / 0.52 / 0.96 / 1.00

What the counts can and cannot bound.

Synthetic checks (source/test_commitstats.py → results/synthetic-checks.json):

10. Limitations

11. Conclusion on the evidence ladder

Rung Supported?
Temporal association between pushes and requests Not detected (p = 0.15, driven by one day). One declared-crawler burst 20 minutes after one push (Oct 1), and three single browser-like visits
Prompt response to publication Not observed: 0 of 48 episodes for the dominant cohort within 1 h. The Oct 1 burst is a single occurrence, ignored the next push, and resembles bursts that had no push nearby
Preferential fetching of novel commits Not observed: the cohort requested old commits about 7 times as often per available commit-hour (over 7 d vs 6–24 h), and never reached most new ones
Repeated monitoring behavior Not observed: no actor repeatedly fetched new commits soon after pushes. Intermittent monitoring, polling that never named commits, and activity outside these logs remain unresolved
Identity or intent of any operator Not addressed by these data

12. A prospective design, if wanted

The historical data cannot test a monitor that would fire only when a crawler is active. For a clean test:

This requires creating artificial commits, which was not done here and needs the author’s go-ahead.

13. Revisions after review (2026-10-20)

An independent spot-check (SPOT-CHECK.txt) reproduced the primary p, the window sums and the 90,122 count, and found the following. Every fix was re-run from the frozen tables; the primary null file is bit-identical.

  1. Conclusion. It now reads as observed behavior: no convincing evidence of repeated prompt fetching. It no longer excludes monitoring in general, and the mechanism language is softened from “link-following, not a trigger” to “consistent with link traversal”.
  2. Estimands. The summary had mixed the adjusted excess (+2.36) with the raw leave-one-out mean (+0.55). Raw and adjusted are now reported side by side throughout, including without Oct 13 (raw +0.55, adjusted −0.49).
  3. Detection simulation.
  4. Binomial bounds. They are now presented as illustrations under an independence model, and renamed in results/power.json. The episode denominators are now computed from the data, with the Oct 4 exclusion named.
  5. “Per novel commit” row.
  6. Removed history. Known removed hashes (6,615 GETs, 4,960 returned 404) and unresolved hashes (2,873 GETs, 2,132 returned 404) are now separate. Combined, 7,092 of 9,488 returned 404 and 2,337 returned 403.
  7. Dependence. A whole-date cluster bootstrap is added: −1.97 to +10.47 for the adjusted excess.
  8. Minor.

14. Reproduction

Frozen inputs.

Software.

Commands (from this directory; PY=/home/jeans/Repos/personal/levineuwirth.org/.venv/bin/python):

# 1. Read-only VPS collection (sanitized), then reconstruction
ssh -F ~/.ssh/config -o BatchMode=yes vps python3 - < source/collect_publication.py > data/publication-snapshot.json
#    data/github-push-events.json: gh api repos/levineuwirth/levineuwirth.org/events (kept fields only)
python3 source/reconstruct_publication.py
# 2. Read-only VPS aggregation (about 2.5 min), then tables
ssh -F ~/.ssh/config -o BatchMode=yes vps nice -n 10 python3 - < source/aggregate_requests.py > PRIVATE/aggregate.json
python3 source/build_tables.py PRIVATE/aggregate.json
# 3. Statistics and figures
$PY source/test_commitstats.py
$PY source/analysis.py
$PY source/hash_analysis.py
$PY source/power.py
python3 source/exploratory_bursts.py
MPLCONFIGDIR=$(mktemp -d) $PY source/plots.py
# 4. Private source check, aggregates only
source/run_private_sources.sh | python3 source/summarize_private_sources.py

Caveats for re-running.

Files.

Methods sources