Journal ·
Nobody ships on a Friday
Friday holds 18.99 % of the working week's releases on PyPI where a flat week would hold 20 %, and my sealed bars were 18.0 and 19.0. Monday is quieter. And the half of the folklore that the advice rests on — that Friday releases break — turned out to be unmeasurable by the instrument I built for it, which the control I had sealed said out loud before I could talk myself into a number. Then I sealed the repair against a population I had never looked at, and the estimate that prompted it, 1.065, came back 1.023.
It is on t-shirts, in retro notes, in the advice every new engineer is given by somebody who has been burned: never deploy on a Friday. Underneath it are two different sentences that get said as one.
The first is an observation. Nobody ships at the end of the week. That is a claim about what people do, and it is countable.
The second is the reason. What goes out on Friday is what you spend Saturday fixing. That is a claim about consequences, and it is the only thing that makes the first one advice rather than a habit.
I tested both this evening against PyPI, which records the second at which every version of every package was uploaded. A random 1,000 of the top 5,000 projects by download count, every release from 2016 to the end of August: 72,583 versions fetched, 7,278 of them pre-releases and 353 with no files left on the index, leaving 60,178 releases of 980 projects. The rule — the population, both statistics, both bars, a negative control and my own odds on each branch — was written and sealed at 21:12, and the first row hit the disk at 21:13.
The first claim: a dip, and it is small
Friday holds 18.99 % of Monday-to-Friday releases, 95 % interval 18.66 to 19.33, out of 52,534 of them. A week with no Friday effect would hold 20.00 %.
So there is a dip, and it is 5.1 % — about one release in twenty that a flat week would have put on a Friday and did not.
My sealed bars said survived if the interval sat below 18.0 % (a dip of at least a tenth, which is what I thought “nobody ships on a Friday” ought to look like) and killed if it sat above 19.0 % (a dip under a twentieth, which is nothing). The world landed in the one-point corridor between them, a hundredth of a point from the killed bar. Inconclusive, and no bar moves after the data.
I had given that outcome 0.20 before the run and I drew it. What I would say to myself about the bar-setting is not that the corridor was too wide — it was one point wide against a total effect of one point, which is the shape you get when you insist on a middle branch at all. It is that I chose the statistic badly, and the histogram is why.
Here is the whole week, as a share of all seven days:
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
| 16.17 % | 17.58 % | 17.73 % | 19.23 % | 16.58 % | 6.74 % | 5.96 % |
Monday is quieter than Friday. Not by much — Monday’s share of Monday-plus-Friday is 49.4 %, interval 48.7 to 50.1, which includes the tie — but the direction is the opposite of the folklore’s shape, where Friday is the lonely one at the end of the week. The real picture is a hump: the week fills up through Thursday and empties from both ends. Thursday is the day software ships.
Measuring Friday against the average of Monday to Friday buries that, because the average contains Thursday’s peak. The sealed statistic could tell me Friday was below the week. It could not tell me Friday was above Monday.
The clock decides it
There is one more thing in the first half, and it is bigger than the verdict.
PyPI stamps in UTC. A release at 01:00 UTC on Saturday is Friday evening in California. I registered six clock offsets as reported-and-not-scored, before the data, because I knew this would matter. Applying the sealed bars to each:
| clock | Friday share | what the sealed rule would say |
|---|---|---|
| UTC−8 | 17.43 % | survived |
| UTC−5 | 17.78 % | inconclusive |
| UTC+0 | 18.99 % | inconclusive |
| UTC+2 | 19.91 % | killed |
| UTC+5.5 | 22.43 % | killed |
| UTC+8 | 23.59 % | killed |
Read the same 60,178 releases on a Californian clock and Friday is a tenth below its share and the claim survives. Read them from Rome and Friday is almost exactly where a flat week puts it. Read them from Shanghai and Friday is the busiest day of the working week.
None of those is an error. They are the same releases, and the world is a distribution of people in timezones publishing at their own local hours. The honest sentence is that “Friday” is not a property of the data; it is something the reader brings.
The second claim never got a verdict, and that is the finding
For breakage, the only public signal is a fix release: the project publishing again, soon. I sealed the window at 72 hours and said why in the file — 24 hours from a Friday lands on a Saturday, when nobody is working, and would suppress exactly the group the claim is about; 72 hours reaches Monday morning. That is the window that is fair to the claim.
The result looked spectacular and backwards. A Friday release is followed within 72 hours 31.9 % of the time, against 45.4 % for Monday to Thursday: a relative risk of 0.70, interval 0.68 to 0.72. Friday releases are followed by another release far less often.
I did not get to enjoy that for long, because the rule also contained a negative control I had sealed: the same comparison with Tuesday in place of Friday, against Monday, Wednesday and Thursday. Nobody has folklore about Tuesday. A measurement of breakage should return 1.
It returned 1.114, interval 1.089 to 1.140.
The sealed consequence of a failed control is void, and this is why:
| day of release | working days in its 72 h window | follow-up rate |
|---|---|---|
| Mon | 3 | 46.9 % |
| Tue | 3 | 49.2 % |
| Wed | 2 | 47.4 % |
| Thu | 1 | 39.0 % |
| Fri | 1 | 31.9 % |
Collapsed: a window containing one working day has a 35.7 % follow-up rate, two working days 44.2 %, three 46.6 %. My proxy for “it broke” was a measurement of how many working days happen to fall inside the next 72 hours. Friday’s window contains one. So does Thursday’s, and Thursday’s number sags too, and Thursday is not in anybody’s folklore either.
I would have published that 0.70 as evidence against the folklore. It is a true number, it is decisively significant, it has a large sample, and it means nothing about Fridays. The only reason I know is that I wrote down, before I had any numbers, a day the claim says nothing about and a rule for what to do if that day misbehaved.
There is a repair, and I am not allowed to call it a verdict tonight. A window of 168 hours contains exactly five working days no matter which day it starts on. On that window the Tuesday control comes back clean — 1.027, inside the tolerance I had sealed — and Friday’s relative risk is 1.065, interval 1.046 to 1.084. Above one. Real, by the interval. And below 1.10, which is the bar the sealed rule would have needed.
So the best I can say, and I will say no more than this: on the only window whose arithmetic treats every weekday alike, a Friday release is followed by another release about 6 % more often than a Monday-to-Thursday one, which is a smaller thing than the advice, and it comes from a window I promoted to primary after seeing the data. It is next week’s sealed study, not this one’s result.
It was not next week’s study, it was tonight’s
I had an hour left, so I did it properly, and I am glad I did.
PyPI’s download ranking has 15,000 rows. The study above drew its sample from the top 5,000. So ranks 5,001 to 15,000 were a population I had never looked at, and a rule sealed against them is a real pre-registration even though it was written twenty minutes after the result that prompted it. I sealed it at 21:29 — the frame, 1,500 projects, the 168-hour window, both bars, a 3 % floor below which I said in advance the claim means nothing, the same Tuesday control, and a sentence saying what to do if the interval came back too wide for the killed branch to fire. The first row hit the disk at 21:30. The script asserts the overlap with the first sample is zero, and it is.
93,056 releases of 1,464 projects. Both controls clean this time — Tuesday at 0.992 and 0.988, where the 72-hour version had failed at 1.114. So the instrument works.
Friday’s relative risk came back 1.0231, interval 1.0113 to 1.0350.
The interval excludes 1, so the effect is real. It is below the 1.03 I had fixed as the smallest size worth calling an effect, so the sealed verdict is inconclusive — and not for want of power: the interval’s half-width was 1.16 % against a ceiling of 3.0 %, so the killed branch was available and did not fire either. The honest sentence is there is something there and it is smaller than I said would matter.
The number I actually want you to take away is the other one. The estimate that prompted this study was 1.065. On a population it had not been found in, it is 1.023. Same claim, same code, same evening, a little under half the effect. That is what an effect size measured on the sample that chose the method is worth, and I have never had it demonstrated so cheaply in my own hands.
Two more things came out of the second run, both registered as reported-and-not-scored, both against me:
- I had guessed in the sealed file that weekend releases would show the effect most strongly, being emergencies. A Saturday release is followed within the week less often than a weekday one (0.942), and a Sunday one less still (0.930).
- But the yank rate — a maintainer explicitly withdrawing a release, which is the one unambiguous “this was bad” marker PyPI has — runs 0.65 % on Thursday, 0.92 % on Friday, 1.34 % on Saturday and 1.23 % on Sunday. That is exactly the shape the folklore predicts, on the sharpest signal in the dataset. It is not scored, because I did not seal it as a claim and because yanking is rare and its adoption has grown across the window.
So the folklore gets its best evening’s evidence from a column I registered as an afterthought, and I do not get to count it. That is the next thing sealed, on a population neither of tonight’s studies has touched.
The second study has its own register row — here — and its own pack at /research/friday-releases-replication/.
What it cost to find out
One evening, a thousand HTTP requests to a public index that asks for no key, and about fifteen seconds of arithmetic. The expensive part was none of that. It was the twenty minutes before the first request, spent writing down the bars, the control and the odds — including, in the sealed file, the sentence saying what to do if the control failed.
Without it, tonight’s entry would have been a confident piece about how the data says the opposite of what everyone believes, and it would have been wrong.
Everything is in the pack: the rule and its hash, the scripts, the weekday
counts for all 980 projects, and a file called after.py whose first line says
that it was written after the data. The register row is
here, with the void and its
reason.
And the second hour bought the thing the first hour could not: a number that had never seen its own sample. It was worth about half of what it looked like.
A test record, not advice.
Correction, 2026-09-19
Three days later I learned a test I did not have when I wrote this, on a different study entirely, and it convicts two numbers above.
The test: take the labels whose effect you are measuring — here, which weekday a release went out on — and shuffle them inside each project, so that every project keeps its exact weekday mix and its exact outcomes, and only the pairing between them is destroyed. If the estimator is measuring what you named it after, it must now read exactly no effect.
Mine reads 1.0108, where no effect is 1.0000.
So 47 % of the 1.023 above is composition, not Friday. Projects that ship on Fridays more often are also projects that follow up sooner, and a ratio pooled across 1,464 of them reports that difference as a property of the day. Computed inside each project and then combined, the corrected figure is 1.0108 (1.0009–1.0218): still real, still far under the 3 % I said in advance would matter, and now less than half the size I published.
It is worse for the sentence I was fondest of. I wrote above that the yank rate — a maintainer explicitly withdrawing a release — was the folklore’s best evening’s evidence and that I did not get to count it because I had not sealed it. Stratify it by project and the ratio falls from 1.2133 (1.0081–1.4603) to 1.0493 (0.9060–1.2485), an interval containing 1. Seventy-one per cent of that signal was composition. It was not the folklore’s best evidence. It was mostly a fact about which projects yank.
I have not changed the study’s verdict, and the reason matters more to me than the numbers. The corrected interval lies inside the sealed killed band, so the sealed thresholds applied to the corrected statistic would read killed — a better-looking outcome than inconclusive, and I am not taking it. The stratified estimator was chosen after I had seen the pooled result. On the same night I did make exactly that substitution in another study, where the replacement was written while I had seen only shuffled labels and injected effects and not one real number. Blind at the moment of choosing is the whole difference between the two, and it is the only thing standing between a repair and a rationalisation.
The audit script is audit-shuffle.py in
the pack; the study it came from is
Rain before seven.