10 interesting stories served every morning and every evening.

Building from Zero After Addiction, Prison, and a Felony

gavinray97.github.io

Building from Zero After Addiction, Prison, and a Felony

I spent ages 14 – 16 in a max­i­mum-se­cu­rity ju­ve­nile prison, be­came a felon at 19, lost al­most every­thing to ad­dic­tion, and later re­built my life through soft­ware, open source, and a few peo­ple who took a chance on me.

I’ve wanted to write this for a while, but kept find­ing rea­sons not to. It felt too per­sonal, too risky, and too easy to mis­read.

Recently, I de­cided on two things:

After see­ing Preston Thorpe speak pub­licly about his own back­ground, I won­dered how many oth­ers like us were silently lurk­ing in tech

I’m far enough in my ca­reer with enough con­tri­bu­tions to OSS and com­mu­nity in­volve­ment, that I think I’ll prob­a­bly be al­right

I wrote this for any­one qui­etly won­der­ing whether they have no chance at a fu­ture.

Below is the much-con­densed life story of my strug­gles with ad­dic­tion, poverty, and in­car­cer­a­tion + life af­ter be­ing a felon. My hope is that it serves as en­cour­age­ment to oth­ers who are in sim­i­lar cir­cum­stances that things CAN get bet­ter.

Amphetamine Addict and Prison at 14

I was a model stu­dent up un­til around pu­berty and mid­dle school. Then, I think a com­bi­na­tion of be­ing bul­lied for be­ing over­weight and teenage hor­mones, led me to be just the wrong com­bi­na­tion of re­sent­ful, an­gry, un­happy, and re­bel­lious.

I started get­ting in fist­fights with peo­ple that made fun of me, be­ing a huge ass­hole to teach­ers, stopped do­ing school­work, and started ex­per­i­ment­ing with drugs.

The be­gin­ning of the end: The day I bought an Adderall from a class­mate. When that am­phet­a­mine feel­ing kicked-in, it was as if life was per­fect for the first time. I was happy, con­fi­dent, felt I could do any­thing. I wanted to feel this way every wak­ing mo­ment for the rest of my life.

Being 14, I had no job, and I do not come from money. So, log­i­cally, I did the thing one must do if one wishes to sus­tain a drug habit: Devise a way to make money.

The eas­i­est way to make money at 14 turned out to be deal­ing drugs, so I started sell­ing var­i­ous pre­scrip­tion med­ica­tions on a buy-low-sell-high” ba­sis from other stu­dents at school.

This was short-lived, as I had the huge mouth of a re­bel­lious I’m in­vin­ci­ble” 14-year old boy, and I was shortly ar­rested and charged with 17 counts of Possession with Intent to Manufacture or Distribute a Scheduled II Controlled Substance.

I wound up spend­ing 2 years, from 14 – 16 at a max­i­mum se­cu­rity ju­ve­nile prison (Lookout Mountain YSC, Golden CO).

Freedom - Shortly Lived

In prison, I got my GED, and af­ter re­lease briefly en­rolled in com­mu­nity col­lege. I was work­ing as a land­scaper do­ing man­ual la­bor for $8/hr and then rid­ing a bus 1hr each way to night classes. Not to say this sort of thing can’t be done (people do it all the time), but I did­n’t have the tenac­ity or mo­ti­va­tion to keep it up, so I dropped out.

I stayed sober for a brief pe­riod be­tween 16 – 17. Not hav­ing learned my les­son, I again started sell­ing drugs. I had learned about The Silk Road and the Darknet and was or­der­ing (what was then) a le­gal Research Chemical” with ef­fects sim­i­lar to MDMA (Methylone/bk-MDMA) shipped to my par­ents house. Eventually, my dad got home early from work and in­ter­cepted a pack­age. Asking me what it was be­fore I left for work, I told him I don’t know, never heard of the re­turn ad­dress name”. My fa­ther was not an id­iot; he told me he was go­ing to open it while I was at work, so I con­fessed it’s drugs.”

Cue huge ar­gu­ment, him in­sist­ing he was go­ing to re­move every­thing from my room ex­cept my clothes and bed (most of which I paid for my­self) and I would not be al­lowed to leave ex­cept for work. This was not an agree­able cir­cum­stance to me, so I re­fused — at which point my dad said then you won’t be liv­ing here any­more!”.

It’s im­por­tant to note that in Colorado (at the time, at least), eman­ci­pa­tion of a mi­nor was not a sta­tus one could file for, but in­stead purely a court sta­tus to be rec­og­nized dur­ing le­gal pro­ceed­ings. That meant there was tech­ni­cally no av­enue for me to legally move out be­fore 18 with proper le­gal sta­tus.

So this, to me, sounded like sweet free­dom & re­lease, rather than a pun­ish­ment. You re­ally won’t call the po­lice if I leave?” Nope.” I packed my back­pack with my lap­top and cash sav­ings, and a suit­case with my clothes, and left. I had no plan but that was a bridge to be crossed.

It turned out that the par­ents of a friend had an un­used bed­room in their trailer they would rent to me un­der-the-table for $300/mo. I jumped at that and slept on the floor of a trailer for 6 months.

I worked as a land­scaper, at a lum­ber mill, and as a cashier at Walgreens, con­tin­u­ing to sell drugs on the side.

Inevitably, I wound up be­ing ar­rested again on drug-re­lated charges, and spent 18 – 19 in county jail. It was then that I be­came a con­victed felon with a low-class felony.

A Serendipitous News Article & a Software Job

While I was in county jail, one day the news­pa­per had a small ar­ti­cle in it: Tech com­pany of­fers in­tern­ships to at-risk & un­der­priv­i­leged youth”

I had spent my child­hood on the com­puter, play­ing videogames and even­tu­ally teach­ing my­self to pro­gram to make game mods. I knew from a young age I wanted to be a pro­gram­mer (I thought I wanted to make videogames, as most young chil­dren do).

This to me, seemed like a for­tu­itous op­por­tu­nity. I cut the ar­ti­cle out and put it in a doc­u­ments folder.

Eventually, I was moved from reg­u­lar jail pop­u­la­tion into the Work-Release jail pro­gram, where they let you out dur­ing the day for work. You had 1 week to find a job, and if you could­n’t se­cure em­ploy­ment you were sent back per­ma­nently to fin­ish out your sen­tence.

The first day out, I walked into the of­fices of the com­pany from the ar­ti­cle and asked to speak to some­one. I ex­plained that I was fresh out of jail and had seen their ar­ti­cle while in­side.

They in­ter­viewed me, de­cided to hire me, and I was now an in­tern Full-Stack Web Developer! I knew noth­ing of web dev, and did­n’t even par­tic­u­larly have an in­ter­est in it orig­i­nally, but the job was al­ready be­yond my hopes. I had as­sumed I was go­ing to spend the rest of my life work­ing con­struc­tion or sim­i­lar, be­cause of my felony.

The same news re­porter that had done the orig­i­nal ar­ti­cle later came to visit, and af­ter in­ter­view­ing me, did a whole writeup on it!

https://​www.dai­ly­cam­era.com/​2017/​05/​12/​boul­der-tech-acad­e­mies-swamped-as-they-race-to-re­train-work­ers/

Working at Techtonic was the best pos­si­ble early-ca­reer ex­pe­ri­ence I think any­one could have had. They did con­tract de­vel­op­ment, a lot of which was green­field Saas MVP launches, across var­i­ous tech stacks. There was not a lot of time for men­tor­ship so it a very trial-by-fire” ex­pe­ri­ence — ei­ther fig­ure things out and ship stuff, or get the boot.

I learned fron­tend, back­end, and dev-ops while there and worked across sev­eral lan­guages + DBs. This was around the time Ruby on Rails + MongoDB was the hip thing. ES6 JS was still fresh and new, and it was there that our CTO did a com­pany meet­ing on this new thing called React” that we were to start learn­ing to re­place jQuery.

It’s also where I met my now-wife, who I pulled into my drug use and un­sta­ble life.

Drugs, Part 2: Electric Boogaloo

Being pos­si­bly the most hard­headed in­di­vid­ual in the uni­verse, I fell back into drug use shortly there­after. I man­aged to re­main mostly-func­tional, un­til the man­ager at Techtonic (who did not like me) lied to the owner that I was show­ing up hours late every day.

She fired me (and my now-wife), and I was later re­deemed when they found the truth in his Slack mes­sage his­tory af­ter fir­ing him many moons later. But oh whale, them’s the breaks”, as they say.

Not hav­ing a job, I spi­raled harder into ad­dic­tion, and even­tu­ally ran out of money to pay my rent and bills. We moved in with my bi­o­log­i­cal fa­ther in Florida. He was also an ad­dict, and in­stead of sta­bil­ity, the sit­u­a­tion be­came en­abling and de­struc­tive. It ex­ploded in short or­der.

From Zero

After the liv­ing sit­u­a­tion with my fa­ther ex­ploded, I was for­tu­nate enough to have a friend who had a spare room in the house and agreed to let me and my (now wife) stay with them for some tiny sum of money, but only tem­porar­ily un­til we could find work + save enough to move out and get back on our feet.

It was at this point we had noth­ing: A few dol­lars to our name, no ve­hi­cle, some cloth­ing and a sin­gle lap­top.

I had lost every­thing. And I had dragged this poor woman into it with me who had lost every­thing, too.

It was at this point my so­bri­ety be­gan. I had hit what we ad­dicts call a bot­tom”. Not the first one, but the one that was fi­nally grim and bleak enough to make me look at my­self and go What the fuck are you do­ing?” The one that fi­nally knocked it into my skull that I did­n’t want to live like this any­more.

I started wash­ing dishes at a restau­rant, and my wife took a job de­liv­er­ing and in­stalling large ap­pli­ances (ovens, fridges, etc) at the same ware­house where the friend worked. Having no ve­hi­cle, she had to bor­row the friend’s bi­cy­cle and ride 30 min­utes in the dark be­fore work, and 30 min­utes in the swel­ter­ing heat af­ter work home. The hours were very long, be­cause it was of­ten on-site in­stal­la­tion paid by the ap­pli­ance, so many days she would work 10 – 12 hours + 1 hour bike ride, and come home so ex­hausted all she could do was sob a lit­tle be­fore get­ting just enough sleep to do it all over again the next day.

Eventually, she told me that it made more sense for me to quit my job while she worked, so that I could spend all of my free time try­ing to get an­other tech job. So she alone car­ried us for sev­eral months. I sent out hun­dreds of ap­pli­ca­tions. I went through fi­nal-round in­ter­views and re­ceived of­fer let­ters from 8 com­pa­nies, only to have them re­scinded each time due to cor­po­rate No Felons” HR poli­cies. It was like hav­ing the car­rot dan­gled right in front of my face, to be snatched away each time.

Finally, I got an in­ter­view with a tiny startup in Miami. I passed their phone screen, and drove 4 hours each way to do the in-per­son.

They of­fered me the job, and helped pay for us to re­lo­cate and tem­porar­ily stay in Airbnb’s. It paid $50k, with the promise of a sig­nif­i­cant raise in 1 year when the com­pany had more rev­enue. I was over­joyed with the of­fer and im­me­di­ately ac­cepted.

Hasura, Open Source, and the Door That Stayed Open

The sys­tem at work was an age­ing Rails app that had ac­crued sig­nif­i­cant tech debt and was the re­sult of an amal­gam of out­sourced de­vel­op­ment shops. One of them was clearly quite pro­fi­cient, and the oth­ers… not so much. Part of my job was de­sign­ing and im­ple­ment­ing a V2 rewrite. While eval­u­at­ing tech­nolo­gies for this, I stum­bled upon Hasura

https://​github.com/​ha­sura/​graphql-en­gine

Put sim­ply, it au­to­mated the work of gen­er­at­ing CRUD for Postgres apps, and was de­signed by peo­ple who clearly had hit the lim­i­ta­tions of tra­di­tional Backend-as-a-Service type plat­forms. Only core CRUD was au­to­mated, and you in­te­grated the rest of your app through wiring up your own API end­points and im­ple­ment­ing your own AuthN + AuthZ.

The first time I plugged in our lo­cal­host Postgres URL for dev, and had a full work­ing CRUD API, I was hooked. Coming from a back­ground of rapidly churn­ing out SaaS MVPs, this was solv­ing a very real prob­lem for me, and it was PERFORMANT.

I be­came heav­ily in­volved in the Discord server, an­swer­ing other peo­ple’s ques­tions, and also started send­ing PRs to im­ple­ment fea­tures I felt were miss­ing.

When my 1 year an­niver­sary came around at work, the founders un­for­tu­nately still were not in a po­si­tion to pay me much more. I knew the fi­nan­cials of the busi­ness and they weren’t ly­ing, but it was still some­what of a dis­ap­point­ment. One of the Hasura em­ploy­ees had re­cently made a joke that I should just ap­ply to work there. I fig­ured that it could­n’t hurt to at least get more info.

I went through the in­ter­view rounds more as a for­mal­ity and was given an of­fer let­ter. I was of­fered slightly more than dou­ble my cur­rent salary! I gen­uinely loved work­ing with the founders at my cur­rent job and felt ter­ri­ble about leav­ing, but I did ac­cept the of­fer and stay on for an­other month to fin­ish up cur­rent work and make sure there was some­one to hand off to.

The com­pany was so small back then that there was no back­ground check done dur­ing the in­ter­view process. After work­ing there a while, I even­tu­ally dis­closed to the Hasura founders that I had a low-grade felony, and, thank the stars, they were cool with it.

I had my dream job: Working on a de­vel­oper-fac­ing tool I gen­uinely loved and was a power-user of, that was also part of the Postgres ecosys­tem. I could not have con­ceived of such a per­fectly-fit po­si­tion. I have been work­ing at Hasura (now PromptQL) since 2020, and I plan on rid­ing this one all the way to it’s end: ei­ther fired, bank­rupt, or bought-out. (Hopefully bought-out).

Conclusion

I don’t tell this story be­cause I think it is clean, heroic, or uni­ver­sally ap­plic­a­ble — It is­n’t. I made TERRIBLE choices. I hurt peo­ple who loved me. I wasted chances that other peo­ple would have killed for. And even when I fi­nally started do­ing the right things, I still needed luck, help, tim­ing, for­give­ness, and peo­ple will­ing to judge me by what I could do next in­stead of only by what I had done be­fore.

But that is ex­actly why I wanted to write this.

If you are read­ing this from the mid­dle of ad­dic­tion, poverty, a crim­i­nal record, or some other hole that feels per­ma­nent: I won’t in­sult you by claim­ing it’s easy. It may be un­fair for a long time. You may have to hear no” from peo­ple who never even look at your work. You may have to re­build with less room for mis­take than every­one around you.

But you are not nec­es­sar­ily fin­ished.

And if you are in a po­si­tion to hire, men­tor, re­view pull re­quests, or let some­one into a room they nor­mally would not be al­lowed into: please re­mem­ber that tal­ent is not evenly dis­trib­uted by back­ground check. Sometimes the per­son who looks risky on pa­per is also the per­son who will spend years try­ing to be­come wor­thy of the chance they were given.

I am alive, sober, mar­ried, em­ployed, and work­ing on soft­ware I care about be­cause a hand­ful of peo­ple took that risk on me.

I wake up grate­ful for that every day. And I hope, over time, to be­come the kind of per­son who gives that same chance to some­one else.

AI Use Disclaimer: claude code was used to gen­er­ate the OpenGraph SVG im­age.

No part of the prose was ma­chine-gen­er­ated. You will not find ma­chine-writ­ten prose on this blog. I con­sider it deeply dis­re­spect­ful.

Dopamine Fracking

igerman.cc

2026.04.13

8 min read

The act of pump­ing im­mense, dis­pro­por­tion­ate re­sources — money, crowd­sourced math, an­a­lyt­ics, op­ti­miza­tion, min-max­ing, pop­u­lar opin­ion ag­gre­ga­tion, etc. — into a pre­vi­ously ca­sual or com­plex, lay­ered ac­tiv­ity to force­fully ex­tract and squeeze out the purest, most con­cen­trated dopamine hit, with no re­gard for any­thing ex­cept dopamine.

Origin

One late evening while chat­ting on Discord, I coined the term dopamine frack­ing” to de­scribe a phe­nom­e­non that has be­come in­creas­ingly preva­lent in on­line cul­ture — a con­cept which I pre­vi­ously strug­gled to ex­press. It’s a metaphor, be­cause just like in ac­tual frack­ing, it is im­mensely harm­ful to the long-term health and sus­tain­abil­ity of any­thing it is ap­plied to, but in the short term, it can yield a very in­tense and con­cen­trated hit of dopamine (or oil).

I briefly called this sloptimization” — a term which was prob­a­bly coined by AI bros to de­scribe the process of op­ti­miz­ing AI mod­els to pass bench­marks, but it does­n’t quite cap­ture the de­struc­tive na­ture of the prac­tice. I guess you could say that a close al­ter­na­tive would be commodification,” over-consumption,” or industrialization” of the hu­man ex­pe­ri­ence, but… all of these words sound more like ster­ile eco­nomic terms and don’t ac­tu­ally sig­nify how ut­terly dev­as­tat­ing this has been to cul­ture, cre­ativ­ity, and con­nec­tion. I feel like dopamine frack­ing” cre­ates a much more gut­tural, vis­ceral, dis­gust­ing im­age of an oil rig in your brain, or worse, in things you love and cher­ish.

Commodifying the Human Experience

I was in­spired to come up with this af­ter watch­ing a few of Metta Beshay’s won­der­ful videos about drugs in the con­text of their orig­i­nal cul­tural sig­nif­i­cance. He cov­ers a lot of dif­fer­ent sub­stances and their his­to­ries, and I highly rec­om­mend go­ing to his chan­nel in­stead of lis­ten­ing to me (an id­iot) talk about it. In short, there’s a rea­son why cer­tain drugs were used in cer­tain cul­tures for thou­sands of years, but be­came much more ne­far­i­ous and de­struc­tive when they were taken out of that con­text. That rea­son is the in­dus­tri­al­iza­tion and cul­tural era­sure by the Enterprising Capitalist™️.

The same thing has been hap­pen­ing to so much of our cul­ture, hob­bies, and even re­la­tion­ships. For all in­tents and pur­poses, an enor­mous num­ber of peo­ple live on­line. The con­stant search for the next big thing, the next big hit of dopamine, has led to a cul­ture of over­con­sump­tion and ad­dic­tion. Whether it’s com­mu­ni­ties be­com­ing too pop­u­lar, mu­sic be­com­ing too cliché, videos be­com­ing too MrBeast-y,” movies be­com­ing too Marvel, web­sites be­com­ing too flat — all that mat­ters is the dopamine hit. And the long-term con­se­quences are ig­nored. Not out of mal­ice, but be­cause it feels as ad­dic­tive as a com­mod­i­fied drug, and peo­ple are sim­ply try­ing to get their next hit.

I’m not say­ing that the things I listed lack merit or ef­fort: an im­mense amount of work un­doubt­edly goes into any movie, song, or video if it’s made by a per­son or team and not by AI. But at a cer­tain thresh­old, if every­thing con­verges on a sin­gle point, there’s quite lit­er­ally no room for any­thing else in zero di­men­sions.

The Strawberry Example

Perhaps my takes are a lit­tle too on­line, so let’s look at a more re­lat­able ex­am­ple: straw­ber­ries. Strawberries are de­li­cious, and they have a very com­plex fla­vor pro­file. They have hun­dreds, if not thou­sands, of strains, and for every sin­gle in­di­vid­ual straw­berry, there are thou­sands of unique com­pounds that con­tribute to its fla­vor. There are white ones, red ones, some are white on the in­side, some are red, some are sour, some are sweet, some are a lit­tle bit­ter, some are very aro­matic, some are very juicy, some are very firm, some are very soft. Even if the dif­fer­ences within a sin­gle bushel of straw­ber­ries are nigh im­per­cep­ti­ble, the ex­pe­ri­ence of eat­ing one is com­plex and lay­ered. And each and every one of the straw­ber­ries you put in a cake, blend into a smoothie, or eat on its own is, in a way, a beau­ti­fully im­per­fect, unique, ana­log ex­pe­ri­ence. You might not no­tice it, you might not care, but it’s there, and it mat­ters — even if just that tiny bit.

But if you were to de­com­pose a straw­berry, ex­tract the aro­matic com­pound that smells most like a straw­berry, an­a­lyze its for­mula, de­vise a way to syn­the­size it, and make it com­mer­cially vi­able, you could put that in every food as a sub­sti­tute for the metic­u­lous work of col­lect­ing good straw­ber­ries and the com­plex palate one has. It would be much cheaper to man­u­fac­ture, and it would give you a very con­cen­trated hit of straw­berry fla­vor. Most peo­ple would­n’t be able to tell much of a dif­fer­ence, and it would prob­a­bly still be de­li­cious. If you’re not greedy.

In fact, this is ex­actly what hap­pens in the food in­dus­try. They ex­tract the com­pound that gives straw­ber­ries their fla­vor and put it in every­thing from cheap candy to ex­pen­sive desserts.

But it would also com­pletely erase every­thing else about the ex­pe­ri­ence of eat­ing a straw­berry. The tex­ture, the juici­ness, the com­plex­ity of the fla­vor, the im­per­fec­tions, the joy of find­ing a par­tic­u­larly good one, the cos­mic hor­ror of eat­ing a wormy one, the nos­tal­gia of hav­ing your grand­ma’s straw­berry jam with dozens of in­di­vid­u­ally unique straw­ber­ries in it. All of that is lost and con­densed into a sin­gle, pure hit of straw­berry fla­vor. Tasty? Maybe. But it’s not a straw­berry any­more. It’s just a chem­i­cal that kind of tastes like a straw­berry. Soon enough, you for­get what one ac­tu­ally tastes like. Or worse, you pre­fer the chem­i­cals. Or even worse, you can’t even find real straw­ber­ries any­more be­cause the mar­ket is flooded with syn­thetic re­place­ments. Or even worser, the real ones have long gone ex­tinct be­cause no one wanted to grow them any­more when the syn­thetic ver­sion was cheaper and more con­ve­nient. And whoop-dee-doo, you’ve erased about 500 in­di­vid­ual hu­man ex­pe­ri­ences and re­placed them with a sin­gle, shared one. And that’s just straw­ber­ries.

This is what dopamine frack­ing does to cul­ture, hob­bies, and even re­la­tion­ships, which are so much more com­plex be­cause they are so deeply ab­stract. It ex­tracts the most con­cen­trated hit of dopamine and puts it in every­thing, while eras­ing all the com­plex­ity, nu­ance, and beauty that made it spe­cial in the first place. And the more we do it, the more we for­get what the orig­i­nal ex­pe­ri­ence was like, and the more we pre­fer the syn­thetic ver­sion, and the worse off we are. It’s a vi­cious cy­cle that leads to a ho­mog­e­nized, com­mod­i­fied cul­ture that is de­void of mean­ing and con­nec­tion.

Remember that SpongeBob episode where they made Krabby Patties out of goo? Yeah. That.

Conclusion

The worst part about it? This was so in­cred­i­bly easy and con­ve­nient to ig­nore for such a long time. Optimization was seen as a good thing, and the idea of solving” some­thing was seen as a pos­i­tive. I def­i­nitely par­tic­i­pated in it, and I’m sure you or some­one you know has, too. After all, who does­n’t want to solve things? Who does­n’t want to op­ti­mize? But the more this hap­pens, the more we see just how de­struc­tive, dev­as­tat­ing, and un­sus­tain­able liv­ing like this is.

I’ve been grad­u­ally turn­ing off dopamine frack­ing in my life: delet­ing chan­nels and feeds that in­fu­ri­ate me or milk my trig­gers (positive or neg­a­tive), unin­stalling apps, and set­ting bound­aries on what I will and won’t en­gage with and con­sume. Becoming aware of this con­cept has made it eas­ier to nav­i­gate the world. And it’s be­com­ing eas­ier and eas­ier for me to sim­ply stop a video and close a tab when I sense that it’s just try­ing to give me a hit of dopamine. It’s so im­mensely lib­er­at­ing to be able to do that.

I don’t have any so­lu­tions. But aware­ness is the first step, and while it feels triv­ial com­pared to ac­tu­ally do­ing some­thing about it, it’s still a step in the right di­rec­tion. I hope that peo­ple can start talk­ing about this, even if not us­ing the term dopamine frack­ing” — I can rec­og­nize that it’s a lit­tle ec­cen­tric, but hey, we call short-form sludge brain rot,” so why not?

Written by a hu­man.

How's Linear so fast? A technical breakdown

performance.dev

Dennis BrotzkyMay 3, 2026

A few mil­lisec­onds is all it takes to up­date an is­sue in Linear. A tra­di­tional CRUD app do­ing the same thing takes about 300ms. How do they do it? There’s no se­cret sil­ver bul­let to per­for­mance. The re­al­ity is that it’s built from the ground up on the right foun­da­tion, then im­proved by count­less de­ci­sions. My goal is to walk through some of the tech­niques that make Linear feel the way it does and help you im­ple­ment the same.

What I’ll cover

Database in the browser

Database in the browser

Making the first load feel in­stant

Making the first load feel in­stant

The sync en­gine

The sync en­gine

Designed for speed

Designed for speed

Animations

Animations

A quick dis­claimer: I’ve never worked at Linear and have never seen their code. Everything I share comes from my per­sonal ex­pe­ri­ence, study­ing their app, read­ing their blog posts, or watch­ing their con­fer­ence talks. I sim­ply love build­ing web apps and have been us­ing Linear since their beta launch. Also, the ar­ti­cle’s hero im­age comes from a video by Meg Wayne, whose work for Linear is phe­nom­e­nal.

Database in the browser

Most web apps live in­side the same loop. The user clicks. The browser fires an HTTP re­quest. A server queries a data­base and sends it back. The browser re­paints. The end re­sult is a spin­ner, a skele­ton, or a frozen UI for a few hun­dred mil­lisec­onds while the app waits on the net­work.

Linear in­verts the tra­di­tional re­la­tion­ship. The ac­tual data­base the UI reads from is in the browser, in IndexedDB. Mutations ap­ply lo­cally first, then asyn­chro­nously push to the server, which broad­casts deltas back to other clients via WebSocket.

In my opin­ion, this is the most crit­i­cal piece to Linear’s per­for­mance. When your goal is to build a fast web app the biggest bot­tle­neck you will fight is the net­work. Any data sent be­tween the client and server costs hun­dreds of mil­lisec­onds. The best ap­proach is to elim­i­nate the need for a net­work re­quest en­tirely: which is ex­actly what Linear does.

I’ll be re­peat­ing this a lot, but the se­cret to build­ing in­cred­i­ble web apps is by hid­ing all the net­work re­quests from the user. The more load­ing states you can avoid the bet­ter.

Here’s an ex­am­ple of how sim­ple Linear’s re­quests are:

// A tra­di­tional web app up­dat­ing the server async func­tion up­dateIs­sue({ is­sue }) { showSpin­ner(); const re­sponse = await fetch(`/​api/​is­sues/${​is­sue.id}`, { method: PATCH, body: JSON.stringify({ ti­tle: is­sue.ti­tle }), }); const up­dated = await re­sponse.json(); setIs­sue(up­dated) hideSpin­ner(); }

// vs Linear is­sue.ti­tle = Faster app launch”; is­sue.save();

The first line, is­sue.ti­tle = Faster app launch”, up­dates an in-mem­ory data­s­tore (MobX ob­serv­able in Linear’s case) . The sec­ond line, is­sue.save();, queues a trans­ac­tion that their sync en­gine batches and flushes to the server. The key here is that the UI re-ren­ders syn­chro­nously off the lo­cal, in-mem­ory, up­date. There are no spin­ners be­cause there is noth­ing to wait for be­cause the data is synced in the back­round. This is the magic of treat­ing the browser as the data­base for each user.

Tuomas, one of Linear’s co-founders, said this at a con­fer­ence in 2024: Literally the first lines of code that I wrote was the sync en­gine, which is very un­com­mon to what you usu­ally do when you’re a startup.’ From day one, Linear knew the ap­proach they wanted to take and the trade­offs it would take.

I know most peo­ple won’t build a cus­tom sync en­gine like Linear just to make their app feel fast and they don’t need to. For most use cases, li­braries like Tanstack Query and SWR can get sur­pris­ingly close with op­ti­mistic up­dates. Most web apps feel slow be­cause the UI waits for each net­work re­quest to com­plete be­fore up­dat­ing state. For most use­cases the net­work re­quest will suc­ceed so you should take ad­van­tage of that and op­ti­misti­cally up­date your state.

// op­ti­mistic mu­ta­tion with SWR mu­tate( `/api/issues/${issue.id}`, { …issue, ti­tle: Faster app launch” }, false );

// vs Linear is­sue.ti­tle = Faster app launch”; is­sue.save();

The key idea is sim­ple: UI re­spon­sive­ness should not de­pend on net­work la­tency. Users per­ceive speed based on how quickly the in­ter­face re­acts, not how quickly the server re­sponds.

Optmistic re­quests is one of the high­est lever­age im­prove­ments you can make:

elim­i­nate un­nec­es­sary spin­ners

elim­i­nate un­nec­es­sary spin­ners

up­date state im­me­di­ately

up­date state im­me­di­ately

val­i­date in the back­ground

val­i­date in the back­ground

roll­back only if needed

roll­back only if needed

Linear’s foun­da­tion is based on this ex­act prin­ci­pal and it makes the app feel na­tive and fast.

A peek into Linear’s stack

Linear is built on the sim­plest stacks you can find: React, TypeScript, MobX, Postgres, a CDN. There’s no edge data­base, no React Server Components, or no fancy frame­work.

Frontend React + re­act-dom (UI run­time) MobX (observable graph, gran­u­lar re-ren­ders) TypeScript (single lan­guage end-to-end) Rolldown-Vite + plu­gin-re­act-oxc(mid-2025; pre­vi­ously Rollup; pre­vi­ously Parcel) ProseMirror + y-pros­emir­ror (rich text ed­i­tor; Yjs CRDT for live col­lab) Radix UI prim­i­tives (popovers, menus, fo­cus traps) Emotion + StyleX (Emotion run­time + StyleX com­piled to atomic CSS) Comlink (Worker RPC) idb (IndexedDB wrap­per back­ing the lo­cal-first store) graphql-re­quest (GraphQL trans­port to the sync server) Sentry (error mon­i­tor­ing) Inter Variable (single woff2, font-dis­play: swap)

Backend Node.js + TypeScript (single lan­guage for all server code) PostgreSQL on Cloud SQL (issues table par­ti­tioned 300 ways) Memorystore Redis (event bus + cache + sync cur­sors) tur­bop­uffer (similar-issue de­tec­tion, vec­tor db) Kubernetes on GCP (one work­load per con­cern) Cloudflare Workers (multi-region edge proxy)

Other clients Desktop: Electron (same web JS, na­tive chrome) Mobile: Swift (iOS) + Kotlin (a sep­a­rate full reim­ple­men­ta­tion)

Marketing Next.js (static) styled-com­po­nents Inline SVG sprite

The biggest stand­out to me is their de­ci­sion to stick with client-side ren­der­ing. CSR of­ten gets crit­i­cized for slow ini­tial loads, but with the right ar­chi­tec­ture and de­sign it can feel in­stant.

I’m also a big fan of the sim­plic­ity it brings. Keeping the app en­tirely client-side cre­ates a much cleaner men­tal model and re­moves a lot of the com­plex­ity that comes with server-ren­dered apps. You don’t have to con­stantly think if you’re on the server or client. If win­dow ob­ject is ac­ces­si­ble or not. If you’re set­ting the right cache head­ers or not. There’s beauty in sim­plic­ity and the con­straints you’re forced into.

So how does Linear make their client side ren­dered app feel in­stant?

Making the first load feel in­stant

One thing I ob­sess over is the first load, and Linear clearly does as well. For pro­duc­tiv­ity tools es­pe­cially, the time it takes be­fore you can ac­tu­ally start work­ing is one of the most im­por­tant de­tails to con­sider. No one wants to be wait­ing for a new tab to load for mul­ti­ple sec­onds

First, you have to un­der­stand what makes ini­tial loads slow. For a client side app you have to re­quest the in­dex.html, then that re­quests all the JavaScript and CSS, which then runs some sort of au­then­ti­ca­tion, and fi­nally makes some API re­quests to show the app.

Linear’s bundler arc: Parcel, Rollup, Vite, Rolldown

The first step to mak­ing an app feel in­stant hap­pens long be­fore run­time. It starts at build time. Remember, the net­work is the bot­tle­neck, so ship­ping the least amount of JavaScript and CSS is crit­i­cal to fast load times.

From what I can gather Linear has rewrit­ten their build pipeline four times: Parcel → Rollup → Vite → Rolldown. Each mi­gra­tion was dri­ven by the same goal: re­duce the amount of JavaScript and CSS and im­prove the de­vel­oper ex­pe­ri­ence.

From their own blog posts they claim:

50% less code shipped.

50% less code shipped.

30% smaller af­ter com­pres­sion.

30% smaller af­ter com­pres­sion.

Cold-cache page loads got 10 to 30% faster.

Cold-cache page loads got 10 to 30% faster.

Time-to-first-paint of the ac­tive-is­sues view dropped 59% (on Safari).

Time-to-first-paint of the ac­tive-is­sues view dropped 59% (on Safari).

Memory us­age dropped 70 to 80%

Memory us­age dropped 70 to 80%

Most of that came from a com­bi­na­tion of de­ci­sions tar­get­ing only mod­ern browsers, bet­ter dead-code elim­i­na­tion, and ag­gres­sive code split­ting. Dropping legacy sup­port is the big win (no poly­fills, no ES5 tran­spi­la­tion, no nomod­ule fall­back) but the dead-code and chunk­ing work mat­ters just as much.

Even with all of these op­ti­miza­tions, Linear still ships a sub­stan­tial amount of code: roughly 21 MB of mini­fied JavaScript. The dif­fer­ence is that it’s ag­gres­sively code split into hun­dreds of route-level chunks that are fetched on de­mand.

// vite.con­fig.ts (reconstruction; matches ob­served chunk graph) ex­port de­fault de­fineCon­fig({ plu­g­ins: [react()], build: { tar­get: esnext”, // no legacy syn­tax, no poly­fills css­Minify: lightningcss”, mod­ulePre­load: { poly­fill: false }, rollupOp­tions: { out­put: { // One chunk per npm pack­age > ~3 KB. Cache in­val­i­da­tion // be­comes per-li­brary in­stead of per-app-re­vi­sion. man­u­alChunks(id) { if (id.includes(“node_modules”)) { const pkg = id.match(/​node_­mod­ules\/([^/]+)/)?.[​1]; if (pkg) re­turn `vendor-${pkg}`; } }, }, }, }, });

The les­son is­n’t which bundler to pick but the im­por­tance of drop­ping legacy browsers, go­ing na­tive ESM, and code split­ting like crazy. Each step is small. Stacked, they cut Linear’s first-load JavaScript roughly in half and their build time by an or­der of mag­ni­tude.

So, the first se­cret to in­stant load times is re­duc­ing the amount of JavaScript and CSS needed to ren­der some­thing for the user.

Preloading af­ter ini­tial load

Once you’ve split your JavaScript into the small­est chunks pos­si­ble you can start do­ing work in the back­ground.

But hold on, split­ting the bun­dle into hun­dreds of chunks cre­ates a new prob­lem. Each chunk im­ports other chunks, and the browser does­n’t know what those are un­til it parses the en­try script. Without help, the load time­line be­comes a wa­ter­fall: fetch the en­try, parse it, fetch its im­ports, parse those, fetch their im­ports. Every level adds a net­work round-trip, which you want to avoid at all costs.

What Linear does is be­fore any JavaScript runs, the browser sees the en­tire list and fires off the re­quests in par­al­lel. By the time the en­try script reaches its first im­port, the chunks are al­ready in cache.

Here’s what it looks like in the <head /> if their in­dex.html

<script type=mod­ule crosso­ri­gin src=“https://​sta­tic.lin­ear.app/​client/​as­sets/​html.2_JBQs3Q.js></script> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​ven­dor-mobx.Crhy2qQc.js> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​SyncWeb­Socket.Djw6l_Op.js> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​Data­base­M­an­ager.DKss­GAN8.js> <!– …around many more –>

The crosso­ri­gin at­tribute on each pre­load matches the crosso­ri­gin on the en­try script, so the browser reuses the cached fetch in­stead of treat­ing pre­load and im­port as sep­a­rate re­sources. Same trick as the font pre­load, ap­plied to every chunk on the crit­i­cal path.

The cold-load time­line col­lapses from a se­quen­tial wa­ter­fall into a sin­gle par­al­lel batch. The net­work still does the work. It just does it all at once. The beauty of this tech­nique is you’re able to do all this work in the back­ground when the user first hits the lo­gin page. In a few sec­onds the full app is stored in cache and served in­stantly.

It’s ex­tremely im­por­tant to un­der­stand how peo­ple will use your app. Once you have this un­der­stand­ing you can start us­ing it to your ad­van­tage, such as pre­load­ing scripts in the back­ground as Linear does.

The ser­vice worker for even more speed and of­fline ca­pa­bil­i­ties

The rest of the Linear, the route-level chunks for views the user has­n’t vis­ited yet, gets cached in the back­ground by a ser­vice worker. The worker has a pre­cache man­i­fest baked into its source, around 1,200 hashed as­sets cov­er­ing route chunks, icons, and fonts, and pulls them down lazily af­ter the first page load. Within a few sec­onds of hit­ting the lo­gin screen, the full app is sit­ting in cache.

This buys two things. Subsequent nav­i­ga­tions skip the net­work en­tirely; the ser­vice worker an­swers di­rectly from its cache with­out even go­ing through HTTP cache. And the app keeps work­ing when the net­work does­n’t. Combined with the lo­cal-first sync en­gine (which al­ready has the user’s data in IndexedDB), Linear is us­able of­fline. You can read is­sues, cre­ate new ones, edit ti­tles and de­scrip­tions, change sta­tuses. Everything queues in the lo­cal trans­ac­tion store and flushes the next time the con­nec­tion comes back.

Modulepreload is for what the app needs now, par­al­lel-fetched so the browser never blocks on a se­r­ial im­port chain. The ser­vice worker is for what the app needs next.

So, to get load times fast the steps for Linear is to elmi­nate as much code as pos­si­ble, split it into small pieces, and pre­cache it in the back­ground. Again, the goal of all this work is to make net­work re­quests as fast as pos­si­ble or, even bet­ter, elim­i­nate them com­pletely.

Vendor bun­dle com­po­si­tion

I found it in­ter­est­ing that every pack­age Linear uses gets its own chunk, cached in­de­pen­dently. A tra­di­tional ven­dor.js in­val­i­dates the en­tire de­pen­dency graph on any bump. Linear’s chunk­ing turns ven­dor caching from a sin­gle mas­sive file to fine-grained. Bumping a sin­gle de­pen­dency in­val­i­dates one chunk; the rest stay cached.

Seems like a no-brainer and yet an­other de­tail to en­sure fast load times.

Loading mas­sive font files

Font load­ing is one of those de­tails a lot of apps get wrong. The fail­ure modes are vis­i­ble: in­vis­i­ble text for half a sec­ond, lay­out shifts as the real font swaps in, dou­ble-fetched re­sources be­cause the pre­load did­n’t match. Linear’s setup avoids all three:

<!– in <head> of in­dex.html –> <link rel=“pre­load” href=“https://​sta­tic.lin­ear.app/​fonts/​In­ter­Vari­able.woff2?v=4.1 as=“font” type=“font/​woff2” crosso­ri­gin=“anony­mous”> <link rel=“pre­con­nect” href=“https://​sta­tic.lin­ear.app crosso­ri­gin>

@font-face { font-fam­ily: Inter Variable”; font-weight: 100 900; font-dis­play: swap; src: url(https://​sta­tic.lin­ear.app/​fonts/​In­ter­Vari­able.woff2?v=4.1) for­mat(“woff2”); } /* Italic and Berkeley Mono fol­low the same shape, sin­gle woff2 each. */

Variable fonts cover the full 100 – 900 weight axis in a sin­gle woff2, elim­i­nat­ing per-weight re­quests. font-dis­play: swap ren­ders the fall­back stack im­me­di­ately and swaps to Inter when it loads. The trick that’s easy to miss: crosso­ri­gin=“anony­mous” on the pre­load tag. Without it, the browser pre­loads the font, then fetches it again when CSS later ref­er­ences it, be­cause the two re­quests have dif­fer­ent CORS modes. crosso­ri­gin on the pre­load makes the browser reuse the cached one.

This all seems sim­ple, but I’m al­ways sur­prsied at how many apps load fonts in­cor­rectly. Linear is a great ex­am­ple of think­ing through the de­tails and en­sur­ing font load­ing is as fast and ac­cu­rate as pos­si­ble.

Inlined app shell

Another key tech­nique to make the first load feel fast: Inlined in <head/> is just enough CSS to paint the load­ing state with no ex­ter­nal stylesheet fetched. Remember, the net­work is the bot­tle­neck and what you’ll al­ways be fight­ing to make your app feel fast. In this case, Linear elmi­nates a net­work re­quest by in­lin­ing the crit­i­cal CSS re­quired to show the user an app shell.

<style> :root { –bg-color: #f5f5f5; –bg-base-color: #fcfcfd; –bg-border-color: #e0e0e0; –sidebar-width: 244px; } html { back­ground: var(–bg-color); height: 100%; } body { font-fam­ily: Inter Variable”, Arial, Helvetica, sans-serif; }

#appBorders { bor­der: 1px solid var(–bg-bor­der-color); back­ground: var(–bg-base-color); mar­gin: 8px 8px 8px var(–side­bar-width); bor­der-ra­dius: 12px; }

#logo { trans­form: trans­lateZ(0); }

@keyframes lo­goB­ack­ground­Pulse { 0% { opac­ity: 0; trans­form: scale(0.8); } 70% { opac­ity: 1; } 100% { opac­ity: 0; trans­form: scale(1.0); } } </style> <script>performance.mark(“appStart”);</script>

Beyond CSS there is also a bunch of in­lined JavaScript that’s crit­i­cal to load­ing the ini­tial ex­pe­ri­ence.

<script> // Electron con­text — lets CSS branch on na­tive chrome. if (navigator.userAgent.includes(“Electron”) && nav­i­ga­tor.user­A­gent.in­cludes(“Lin­ear”)) doc­u­ment.doc­u­mentEle­ment.classList.add(“elec­tron”);

DeepSeek V4 Pro beats GPT-5.5 Pro on precision

runtimewire.com

The Absurdly Optimized Pancake: Leavening Chemistry, Acid-Base Stoichiometry, and an Interactive Calculator

www.absurdlyoptimized.com

Calculator out­put, plated. The lacy edges are Section IX; the rest is Sections I through VIII.

I have been mak­ing pan­cakes for twenty-five years. It was the first food I ever learned to cook, start­ing with Dorie Greenspan’s recipe from Pancakes: From Morning to Midnight. I made her recipe du­ti­fully for close to twenty years un­til some­one men­tioned Kenji López-Alt’s but­ter­milk pan­cakes, and I switched to mak­ing those du­ti­fully in­stead.

But I started to won­der whether I had ac­tu­ally found the op­ti­mal pan­cake, or just the most re­cently rec­om­mended one. And every time I made Kenji’s recipe I was an­noyed at two things: hav­ing to run out for but­ter­milk (or do men­tal sto­i­chiom­e­try to sub­sti­tute yo­gurt while in a pre-caf­feinated state), and the use of im­pre­cise cup mea­sure­ments rather than weights. I was also cu­ri­ous about com­pet­ing recipes that used sour cream, Greek yo­gurt, cot­tage cheese. Each one claimed to be the best. None of them showed their work.

So I did what any rea­son­able per­son would do. I de­rived the pan­cake from first prin­ci­ples.

Every recipe in every cook­book is a frozen snap­shot of one point in this pa­ra­me­ter space. This cal­cu­la­tor lets you ex­plore the space freely. Change what you have, change what you want, and the sto­i­chiom­e­try adapts.

1. What Actually Matters

A pan­cake has four axes of qual­ity, and most recipes op­ti­mize for at most one of them while ne­glect­ing the other three. In or­der of what you will ac­tu­ally no­tice while eat­ing:

Interior tex­ture. The in­side should be light and cus­tardy, not dense and bready. This is con­trolled by leav­en­ing (both chem­i­cal and me­chan­i­cal), pro­tein struc­ture, and hy­dra­tion ra­tio. A pan­cake that re­quires chew­ing has failed at its only job.

Tang. A flat-fla­vored pan­cake is a ve­hi­cle for maple syrup. A good pan­cake has its own acid bright­ness from resid­ual lac­tic and cit­ric acid that was in­ten­tion­ally left un-neu­tral­ized. This is a sto­i­chio­met­ric de­ci­sion: how much of your avail­able acid to con­sume with bak­ing soda (producing CO2) ver­sus how much to leave be­hind (producing fla­vor).

Rise and struc­ture. The pan­cake should be tall with­out be­ing cakey. This comes from three in­de­pen­dent CO2 sources (baking pow­der, bak­ing soda re­act­ing with acid, and steam from high-mois­ture in­gre­di­ents) plus one me­chan­i­cal source (whipped egg whites). The four sources op­er­ate on dif­fer­ent timescales, which is why they all con­tribute in­de­pen­dently.

Exterior crisp. A thin Maillard-browned shell that pro­vides tex­tural con­trast. Requires sur­face tem­per­a­ture above 140°C, re­duc­ing sug­ars, amino acids, and a mi­cro-fry­ing zone where clar­i­fied but­ter cre­ates rapid sur­face de­hy­dra­tion. The crisp here is built from that Maillard crust and the lacy ghee-fried edges, not from corn­starch: amy­lose gives a brit­tle, glassy shell, but past a small frac­tion it reads as an ar­ti­fi­cial fried-coat­ing crunch rather than a pan­cake crust, so the cal­cu­la­tor leaves it out (with a note for any­one who wants to ex­per­i­ment).

3. Background

Recipe de­vel­oped and kitchen-tested by a hu­man; AI helped with the back­ground re­search.

The old­est con­tin­u­ously pre­pared food

Pancakes are, in all prob­a­bil­ity, the old­est cooked food that mod­ern hu­mans would still rec­og­nize. Analysis of starch grains on 30,000-year-old grind­ing tools from sites in Italy (Bilancino II), Russia (Kostenki 16), and the Czech Republic (Pavlov VI) re­vealed flour made from cat­tails and ferns, likely mixed with wa­ter and cooked on hot stones (Revedin et al., 2010). This is not a pan­cake in the mod­ern sense, but it is a bat­ter cooked on a flat hot sur­face, which is the de­f­i­n­i­tion of one.

Otzi the Iceman (c. 3300 BCE) car­ried einkorn wheat with char­coal par­ti­cles con­sis­tent with flat­cake cook­ing (Maixner et al., 2018). By the 5th cen­tury BCE, the Greeks were mak­ing tegan­ites (from teganon, frying pan”): wheat flour, olive oil, honey, and cur­dled milk, served for break­fast (Athenaeus, c. 200 CE; Albala, 2008). The Roman Ova Sfongia Ex Lacte (“egg sponge with milk”) from Apicius calls for eggs, milk, and oil beaten into a bat­ter, fried, and served with honey and pep­per (Apicius, 4th cen­tury CE).

The word pancake” first ap­pears in Middle English in the 15th cen­tury (Austin, 1888). It be­came as­so­ci­ated with Shrove Tuesday be­cause house­holds needed to ex­haust their eggs, milk, but­ter, and fats be­fore the forty-day Lenten fast. Pancakes ef­fi­ciently com­bined all these per­ish­able in­gre­di­ents into a sin­gle prepa­ra­tion. The Olney Pancake Race in Buckinghamshire has been run since 1445, mak­ing it pos­si­bly the old­est con­tin­u­ously held sport­ing event mo­ti­vated en­tirely by break­fast (Albala, 2008).

The leav­en­ing rev­o­lu­tion

For most of hu­man his­tory, all pan­cakes were thin. A bat­ter of flour, eggs, and liq­uid, cooked on a hot sur­face, pro­duces a crepe. The thick fluffy pan­cake is a 19th-century in­ven­tion made pos­si­ble by chem­i­cal leav­en­ing.

The time­line: pearlash (potassium car­bon­ate, re­fined from wood ash) ap­peared in American kitchens in the 1780s and was the first chem­i­cal leav­ener (Simmons, 1796). Saleratus (sodium bi­car­bon­ate) re­placed it in the 1840s. In 1843, English chemist Alfred Bird cre­ated the first bak­ing pow­der by com­bin­ing bi­car­bon­ate of soda with tar­taric acid and starch, mo­ti­vated by his wife’s al­lergy to both eggs and yeast (Bird, 1843). In 1856, Harvard pro­fes­sor Eben Norton Horsford (a stu­dent of Justus von Liebig) patented mono­cal­cium phos­phate as a bak­ing pow­der acid, elim­i­nat­ing ex­pen­sive im­ported cream of tar­tar and found­ing the Rumford Chemical Works (Horsford, 1856; ACS). Double-acting bak­ing pow­ders (which re­lease CO2 in two stages: once when wet, again when heated) ap­peared around 1890.

The con­se­quence was the thick American pan­cake. Before chem­i­cal leav­en­ing, pan­cakes were struc­turally lim­ited to the thin bat­ter that eggs and yeast could sup­port. Baking pow­der gave bat­ters an in­ter­nal gas source that did not de­pend on yeast fer­men­ta­tion or whipped eggs, en­abling the heavy, high-hy­dra­tion bat­ters that pro­duce a tall, fluffy disc. The first com­mer­cial pan­cake mix (1889, Pearl Milling Company, St. Joseph, Missouri) com­bined wheat flour, corn flour, lime phos­phate, and salt into what is widely con­sid­ered the first ready-mix food prod­uct in com­mer­cial his­tory (Pearl Milling Company, 1889).

What bak­ing soda ac­tu­ally does

Baking soda has a rep­u­ta­tion as a pure leav­ener, and recipe com­ment threads reg­u­larly ar­gue over whether it is there for rise, for brown­ing, or for cut­ting acid­ity. The hon­est an­swer is all three at once, be­cause they are the same re­ac­tion seen from three an­gles. Sodium bi­car­bon­ate re­acts with the bat­ter’s acid to re­lease CO2 (the rise); that same re­ac­tion con­sumes acid and raises the bat­ter’s pH (less tang); and the higher pH then ac­cel­er­ates brown­ing. The three ef­fects are not sep­a­ra­ble knobs. You can­not dial in one with­out mov­ing the other two.

The brown­ing claim is the con­tested one, so it is worth pin­ning down. The Maillard re­ac­tion is not merely catalyzed in both acidic and ba­sic con­di­tions” at some flat rate; its rate climbs steeply with pH. The first and rate-de­ter­min­ing step is a nu­cle­ophilic at­tack by an amino group on the car­bonyl of a re­duc­ing sugar, and an amino group is nu­cle­ophilic only when it is de­pro­to­nated. In an acidic bat­ter most amino groups sit as un­re­ac­tive pro­to­nated am­mo­nium ions, so brown­ing is slow; rais­ing the pH frees them, and the brown­ing rate climbs steeply with pH across the weakly acidic to neu­tral range, with very lit­tle brown­ing be­low pH 6 (Martins & van Boekel, 2005). J. Kenji López-Alt showed the same thing pho­to­graph­i­cally, step­ping up the soda in oth­er­wise iden­ti­cal bat­ters and get­ting vis­i­bly darker pan­cakes each time, un­til the ex­cess soda turned soapy (López-Alt, 2015). So soda does brown, the com­menter who said it was for loft, not brown­ing” had the wrong half, and the one who in­voked an al­ka­line en­vi­ron­ment was right about the chem­istry even if you need it” over­states the case (an acidic bat­ter still browns, just re­luc­tantly, given enough heat and time).

The catch is that the brown­ing and the tang are drawn from the same well. Every in­cre­ment of pH you spend on a darker crust is acid you have neu­tral­ized and tang you have lost, which is the cen­tral con­flict this cal­cu­la­tor is built around. The res­o­lu­tion, de­vel­oped in the method­ol­ogy, is to stop us­ing pH as the brown­ing lever at all and brown by other means (concentrated re­duc­ing sug­ars and ly­sine, a clar­i­fied fry­ing fat) so that the acid can be spent on fla­vor in­stead.

The ri­cotta pan­cake: Sydney, 1993

The ri­cotta pan­cake as a dis­tinct cat­e­gory was cre­ated by Bill Granger (1969 – 2023), who opened his first restau­rant, bills,” in Darlinghurst, Sydney in 1993. He was twenty-two, self-taught, and had stud­ied art. His sig­na­ture ri­cotta hot­cakes with hon­ey­comb but­ter ap­peared in bills Sydney Food (Murdoch Books, 2000) and be­came the defin­ing dish of Australian cafe cul­ture (Granger, 2000).

The in­no­va­tion was struc­tural: ri­cot­ta’s pre-de­na­tured whey pro­teins pro­vide body with­out flour, while sep­a­rated and whipped egg whites pro­vide me­chan­i­cal leav­en­ing. The re­sult is a pan­cake with dra­mat­i­cally less gluten de­vel­op­ment and dra­mat­i­cally more pro­tein struc­ture than any flour-for­ward recipe. The New Yorker cred­ited Granger as the restau­ra­teur most re­spon­si­ble for the Australian cafe’s global reach.” He opened restau­rants in Tokyo, Seoul, and London be­fore his death in December 2023 at age 54.

Global vari­ants and what they re­veal

Every cul­ture with ac­cess to grain and a flat hot sur­face in­vented pan­cakes in­de­pen­dently, and the vari­a­tions re­veal which pa­ra­me­ters each cul­ture op­ti­mized for:

Dutch pan­nenkoeken: Large (30cm), mod­er­ately thin, served as a full meal with sa­vory fill­ings. Optimized for size and ver­sa­til­ity. The ear­li­est men­tion in a Dutch man­u­script dates to 1183 (Albala, 2008).

Russian blini: Small buck­wheat pan­cakes pre­dat­ing Christianity, orig­i­nally pa­gan sun sym­bols. Optimized for rit­ual sig­nif­i­cance and nutty fla­vor from buck­wheat. The Maslenitsa fes­ti­val (Butter Week) main­tains the tra­di­tion (Moscow Times, 2023).

Ethiopian in­jera: Spongy fer­mented teff flat­bread, nat­u­rally leav­ened by 1 – 3 days of wild lac­tic acid bac­te­ria fer­men­ta­tion. Teff has been cul­ti­vated in the Ethiopian high­lands for at least 3,000 years (Mezber/Ona Adi ex­ca­va­tions, 2021). Optimized for serv­ing as both plate and uten­sil.

Japanese souf­fle pan­cakes: Extremely tall, jig­gly, steamed in ring molds. Codified by Gram Cafe (Osaka, 2014). Optimized for height and spec­ta­cle at the ex­pense of Maillard brown­ing (Honolulu Magazine).

Korean hot­teok: Filled with brown sugar, cin­na­mon, and peanuts. Originated from Chinese mer­chants in 1880s Korea. Optimized for tex­tural con­trast be­tween crispy shell and molten fill­ing.

4. Methodology

I. Leavening: four in­de­pen­dent CO2 sources

The four sources at work: in­te­rior crumb from a blue­berry batch. The voids were CO2 and steam; the walls around them are co­ag­u­lated egg pro­tein.

A pan­cake’s rise comes from gas cells ex­pand­ing dur­ing cook­ing. Unlike bread (which re­lies on a sin­gle source: yeast fer­men­ta­tion), an op­ti­mized pan­cake bat­ter uses four in­de­pen­dent gas sources op­er­at­ing on dif­fer­ent timescales:

Source 1: Baking soda + acid (immediate). The re­ac­tion is in­stan­ta­neous upon mix­ing:

\text{NaHCO}_3 + \text{H}^+ \rightarrow \text{Na}^+ + \text{H}_2\text{O} + \text{CO}_2 \uparrow

One mole of sodium bi­car­bon­ate (84 g/​mol) re­acts with one mole of hy­dro­gen ions to pro­duce ex­actly one mole of CO2 (44 g/​mol). At 100°C and 1 atm, one mole of CO2 oc­cu­pies 30.6 L (ideal gas law). This re­ac­tion is the pri­mary rea­son to in­clude acid in­gre­di­ents (buttermilk, lemon juice, yo­gurt): each acid source is si­mul­ta­ne­ously a fla­vor con­trib­u­tor and a CO2 feed­stock.

Source 2: Baking pow­der. A self-con­tained acid-base sys­tem: sodium bi­car­bon­ate plus a pow­dered acid, with corn­starch as a buffer (BAKERpedia). The acid is what mat­ters. Some pow­ders use sodium alu­minum sul­fate, which leaves the faintly metal­lic, bit­ter af­ter­taste peo­ple blame on too much bak­ing pow­der.” Use an alu­minum-free pow­der in­stead; a mono­cal­cium-phos­phate one such as Rumford (cornstarch, sodium bi­car­bon­ate, mono­cal­cium phos­phate; not Clabber Girl, which uses the alu­minum) is the clean­est-tast­ing. Monocalcium phos­phate re­leases its CO2 on wet­ting, so add the pow­der shortly be­fore cook­ing and cook promptly, which both modes do any­way.

Source 3: Steam (thermal). High-moisture in­gre­di­ents (ricotta at 70 – 80% wa­ter, but­ter­milk, eggs at 74% wa­ter) pro­vide a reser­voir of liq­uid that va­por­izes dur­ing cook­ing. Steam is not a chem­i­cal re­ac­tion; it is a phase tran­si­tion. But it ex­pands ex­ist­ing gas cells sig­nif­i­cantly, par­tic­u­larly in the high-mois­ture en­vi­ron­ment of a ri­cotta bat­ter.

Source 4: Whipped egg whites (mechanical). When egg whites are whipped, the me­chan­i­cal force de­na­tures oval­bu­min (the ma­jor pro­tein, 54% of egg white pro­tein mass), ex­pos­ing hy­dropho­bic residues that align at the air-wa­ter in­ter­face to form a sta­ble pro­tein film around each air bub­ble (McGee, 2004). These pre-formed air cells do not re­quire any chem­i­cal re­ac­tion; they are al­ready pre­sent in the bat­ter and ex­pand ther­mally dur­ing cook­ing. Ovalbumin co­ag­u­lates ir­re­versibly at 80°C (Weijers et al., 2003), per­ma­nently set­ting the foam struc­ture.

II. Acid-base sto­i­chiom­e­try: the tang equa­tion

The cen­tral op­ti­miza­tion prob­lem in pan­cake chem­istry is this: acid serves two com­pet­ing pur­poses. It re­acts with bak­ing soda to pro­duce CO2 (desirable for rise), but the un­re­acted resid­ual acid is what pro­vides tang (desirable for fla­vor). You can­not max­i­mize both si­mul­ta­ne­ously. The ques­tion is what frac­tion of avail­able acid to neu­tral­ize.

The avail­able acid sources, their con­cen­tra­tions, and their H+ con­tri­bu­tion at typ­i­cal recipe quan­ti­ties:

Cream of tar­tar is sur­pris­ingly po­tent. Potassium hy­dro­gen tar­trate (KHC4H4O6, MW 188) is a pure dry acid: its acid mass frac­tion is 1.0, mean­ing 100% of its weight par­tic­i­pates in the acid-base re­ac­tion. Compare this to ke­fir at 1.0% acid and 90% wa­ter, or ri­cotta at 0.2% acid and 74% wa­ter. A mere 1.5g of cream of tar­tar (1/4 tea­spoon) pro­vides ap­prox­i­mately 8 mmol H+, which is 57% of the acid tar­get at tang level 4. The in­tu­ition that a tiny pinch can­not mat­ter” is wrong by an or­der of mag­ni­tude. Cream of tar­tar also sta­bi­lizes egg white foam by low­er­ing pH to­ward oval­bu­min’s iso­elec­tric point (4.5), serv­ing dou­ble duty as both acid source and foam sta­bi­lizer.

Each dairy in­gre­di­ent serves up to three in­de­pen­dent roles (structure, hy­dra­tion, acid), and sub­sti­tut­ing 1 cup but­ter­milk for 1 cup yo­gurt” is di­men­sion­ally wrong. The cal­cu­la­tor solves for each role sep­a­rately: ri­cotta and cot­tage cheese are fixed by struc­tural need (pre-denatured whey pro­teins), acidic dairy (kefir, but­ter­milk, yo­gurt, sour cream) is com­puted from the acid tar­get, and milk fills any re­main­ing hy­dra­tion deficit.

Dairy acid sources are prefer­able on every axis ex­cept acid con­cen­tra­tion. They pro­vide lac­tic acid (which pro­duces the char­ac­ter­is­tic tangy pan­cake” fla­vor that cit­ric acid does not), lac­tose (a re­duc­ing sugar for Maillard brown­ing), and pro­tein (including ly­sine, the most Maillard-reactive amino acid). Citric acid in a cooked pan­cake does not pro­duce per­ceiv­able tang; with­out strong cit­rus aroma to con­tex­tu­al­ize it, resid­ual cit­ric acid reads as vaguely sour or goes un­no­ticed. Lemon zest pro­vides dis­tinc­tive cit­rus fla­vor via aro­matic ter­penes (limonene, cit­ral), but the juice’s only role is as a con­cen­trated acid for CO2 pro­duc­tion.

Lemon juice ap­pears in the cal­cu­la­tor only when dairy acid sources alone do not pro­vide enough H+ for the de­sired tang and CO2 bal­ance. With ri­cotta only (5.6 mmol H+), sup­ple­men­tal cit­ric acid is nec­es­sary at mod­er­ate to high tang set­tings. With ri­cotta plus but­ter­milk (15.9 mmol), or ri­cotta plus sour cream and yo­gurt (16.3 mmol), dairy acid is suf­fi­cient and lemon juice is omit­ted. When cit­rus is se­lected and juice is not needed, only the zest is in­cluded for fla­vor.

Citric acid is tripro­tic (three dis­so­cia­ble pro­tons), but the ef­fec­tive ra­tio is ap­prox­i­mately 2.5 rather than 3. The rea­son in­volves pKa val­ues: the third dis­so­ci­a­tion con­stant (pKa3 = 6.40) is nearly iden­ti­cal to the pKa of car­bonic acid (H2CO3, pKa = 6.35). At bat­ter pH (~6.4), by the Henderson-Hasselbalch equa­tion, only ap­prox­i­mately 50% of cit­rate mol­e­cules have sur­ren­dered their third pro­ton. The ef­fec­tive H+ con­tri­bu­tion is there­fore 2 + 0.5 = 2.5 moles per mole of cit­ric acid (PubChem, Citric acid).

The neu­tral­iza­tion cal­cu­la­tion:

n_{\text{soda}} = n_{\text{acid}} \times f_{\text{neu­tral­ize}}

where f_{\text{neu­tral­ize}} is the frac­tion of acid to con­sume (0.30 for max­i­mum tang, 0.80 for mild). The re­main­ing acid pro­vides resid­ual acid­ity:

\text{Residual acid­ity (\%)} = \frac{(n_{\text{acid}} - n_{\text{soda}}) \times M_{\text{lactic}}}{m_{\text{batter}}} \times 100

Perception thresh­old for acid­ity in bat­ter is ap­prox­i­mately 0.05% (lactic acid equiv­a­lent). Above 0.2%, the pan­cake reads as dis­tinctly tangy. For ref­er­ence, wheat sour­dough breads con­tain 0.45 – 0.73% lac­tic acid (Clement et al., 2020), and bread pH (which cor­re­lates with sour taste at = 0.97) ranges from 4.07 to 4.40. The cal­cu­la­tor’s tang slider spans from 0.03% (below per­cep­tion) to 0.51% (solidly in the sour­dough range).

Most recipes get this wrong. A typ­i­cal lemon ri­cotta pan­cake” recipe calls for 1.5 tea­spoons of bak­ing soda (~6.9g, 0.082 mol) with 57 mL of lemon juice (0.037 mol H+) and 227g ri­cotta (0.005 mol H+). The soda ex­ceeds the to­tal acid by a fac­tor of two. Every mol­e­cule of lemon acid is neu­tral­ized. Despite the recipe’s name, the lemon juice con­tributes zero per­ceiv­able tang to the fin­ished pan­cake; all lemon fla­vor comes from the zest. Worse, the ~0.04 mol of un­re­acted NaHCO3 ther­mally de­com­poses dur­ing cook­ing into sodium car­bon­ate (Na2CO3), which is al­ka­line, bit­ter, and soapy. This is the fa­mil­iar metal­lic off-fla­vor of too much bak­ing soda.” The cal­cu­la­tor pre­vents this by com­put­ing the ex­act sto­i­chiom­e­try: it adds only enough soda to neu­tral­ize the de­sired frac­tion of acid, never more.

The sponge af­ter its overnight cool-room fer­ment, pocked with CO2 from the yeast and the ke­fir’s cul­tures. They spent the night on fla­vor, not lift.

The overnight path: fer­ment for fla­vor, soda for rise. At max­i­mum tang the cal­cu­la­tor switches to an overnight fer­ment, but not be­cause the yeast does the leav­en­ing. The rise still comes from bak­ing soda and bak­ing pow­der; the long rest is there to deepen fla­vor and to let the cul­tures push tang past what the sto­i­chiom­e­try sets. This is how yeasted and sour­dough pan­cake recipes ac­tu­ally be­have: a slow fer­ment for char­ac­ter, chem­i­cal leav­en­ing for lift (King Arthur Baking).

Why the leav­ener goes in last. Gas and time do not mix in a loose bat­ter. A mono­cal­cium-phos­phate bak­ing pow­der re­leases its CO2 the in­stant it is wet­ted. Fold it in the night be­fore and that gas es­capes the thin, pourable bat­ter long be­fore morn­ing, leav­ing noth­ing to lift the pan­cake, ex­actly why an overnight sour­dough pan­cake adds its leav­ener fresh in the morn­ing rather than the night be­fore. So in overnight mode the bak­ing pow­der is held back, folded in just be­fore cook­ing, and cooked im­me­di­ately, so its gas goes into the pan­cake in­stead of es­cap­ing the bowl overnight.

Keeping the soda does not cost the tang. The in­tu­itive worry is that soda neu­tral­izes the acid you want for sour­ness, but it does not bite here, be­cause a cul­tured-dairy bat­ter car­ries far more acid than the soda can con­sume. The soda only ever neu­tral­izes the ex­cess above the resid­ual tar­get (the same cal­cu­la­tion the quick bat­ter runs); with sev­eral hun­dred grams of ke­fir in the bowl, what re­mains sits solidly in the sour­dough range. Reliable rise and ag­gres­sive tang at the same time, no fer­men­ta­tion gas re­quired.

The yeast is a fla­vor dose, and the sugar it eats is added back. The yeast in overnight mode is small, a frac­tion of a per­cent of the flour: enough to fer­ment and aer­ate over the long rest along­side the ke­fir’s own cul­tures, not enough to be the leav­ener. It still eats sugar as it works, and here a chem­i­cal-ver­sus-bi­o­log­i­cal asym­me­try mat­ters. Baking soda con­sumes acid, not sugar, so all added sugar sur­vives into a quick bat­ter; fer­men­ta­tion re­moves sugar. Crucially the yeast can­not touch the dairy sugar, be­cause S. cere­visiae lacks the en­zyme to split lac­tose, so it lives on the added su­crose, in­vert­ing it and fer­ment­ing four moles of CO2 per mole. How much it eats tracks the yeast’s gassing rate, which is Arrhenius in tem­per­a­ture (Chiotellis & Campbell, 2003; rate near op­ti­mum per Cauvain & Young, 2007), so a cool-room fer­ment eats more than a cold one. The cal­cu­la­tor adds that amount back on top of the in­tended sugar, leav­ing the same fin­ished sweet­ness and crust brown­ing a same-day bat­ter would have.

Cool room ver­sus fridge. With the rise handed to the morn­ing soda, the fer­ment tem­per­a­ture be­comes purely a fla­vor and con­ve­nience choice. A cool room (about 20°C) keeps the ke­fir cul­tures and yeast ac­tive, so they gen­er­ate ex­tra lac­tic and acetic acid overnight and push tang past the sto­i­chio­met­ric tar­get; the cost is a tighter win­dow, since past about 12 hours or in a warm kitchen the bat­ter turns sour and sol­vent-like. A fridge (about 4°C) nearly stalls the cul­tures, so the tang stays close to what you mixed in, but the tim­ing is for­giv­ing: 8 or 14 hours look alike. Because max­i­miz­ing tang is the whole point of the overnight mode, the cal­cu­la­tor de­faults to the cool room and of­fers the fridge to cooks who would rather have a wide tim­ing win­dow than the last in­cre­ment of sour­ness. The rise is iden­ti­cal ei­ther way, be­cause it comes from the morn­ing leav­en­ers, not the fer­ment.

The overnight fer­ment also de­vel­ops mod­er­ate gluten struc­ture via hy­dra­tion, pro­duc­ing a slight chew that is ab­sent in the quick-mixed chem­i­cal leav­en­ing ver­sion. This is a fea­ture for pan­cakes where some struc­tural pull is de­sir­able (as op­posed to the pure souf­fle tex­ture of whipped-egg-only leav­en­ing). The acid con­cen­tra­tion at tang level 5 (~0.5% lac­tic equiv­a­lent, com­pa­ra­ble to the lower end of sour­dough bread) is far too low to de­na­ture gluten pro­teins overnight, so the chew de­vel­ops with­out degra­da­tion. At the much higher acid­ity of a ma­ture sour­dough (roughly 0.8 – 1.2% lac­tic acid), the gluten net­work does weaken over a few hours, not by any di­rect at­tack on its disul­fide crosslinks but be­cause the low pH raises the pro­teins’ net pos­i­tive charge and sol­u­bil­ity, loos­en­ing their non-co­va­lent bonds, and switches on en­doge­nous ce­real pro­teases that hy­drolyze the glutenin (Thiele et al., 2004), but this thresh­old is never ap­proached in the cal­cu­la­tor’s acid range.

The morn­ing af­ter: a yolk goes into the sponge. The rise still comes from the soda and pow­der added now; the night was for tang.

Kefir’s live cul­tures dur­ing overnight fer­ment. Kefir is not a ster­ile acid source. It con­tains live Lactobacillus bac­te­ria and wild yeast strains that stay meta­bol­i­cally ac­tive in the bat­ter. In an overnight fer­ment, these or­gan­isms pro­duce ad­di­tional lac­tic acid be­yond what was pre­sent when the bat­ter was mixed, mak­ing the fin­ished pan­cake tang­ier than the sto­i­chiom­e­try alone pre­dicts. Like the yeast, their rate is tem­per­a­ture-de­pen­dent: at a cool room (~20°C) the self-sour­ing is mean­ing­ful, while at fridge tem­per­a­ture (bacterial me­tab­o­lism slows roughly 2 – 4x at 4°C com­pared to 25°C) it is small. This is the bi­o­log­i­cal half of why the cal­cu­la­tor de­faults to a cool-room fer­ment when tang is max­i­mized: the cul­tures fin­ish the job the sto­i­chiom­e­try starts. Buttermilk cul­tures be­have sim­i­larly but are less di­verse; ke­fir’s sym­bi­otic colony (the kefir grain”) con­tains dozens of bac­te­r­ial and yeast species com­pared to but­ter­milk’s 2 – 3.

The two modes leaven dif­fer­ently. The quick bat­ter uses bak­ing soda plus bak­ing pow­der, mixed in at the start: there is ex­cess acid above the (low) tang tar­get, and the soda turns it into free CO2. The overnight bat­ter, at max­i­mum tang, uses bak­ing pow­der alone, folded in the morn­ing, be­cause soda would con­sume the very acid the long fer­ment is build­ing into tang; the bak­ing pow­der car­ries the rise from its own acid with­out touch­ing the bat­ter’s. Whipped egg whites add me­chan­i­cal lift in both, but the de­pend­able rise is chem­i­cal.

III. Gluten in­hi­bi­tion: why less flour works

Wheat flour con­tains two stor­age pro­teins, glutenin and gliadin, which to­gether com­prise 80 – 85% of to­tal flour pro­tein. When hy­drated and me­chan­i­cally worked, they bond into gluten: a vis­coelas­tic net­work of cross-linked pro­tein sheets joined by disul­fide bridges, hy­dro­gen bonds, and hy­dropho­bic in­ter­ac­tions (McGee, 2004).

Gluten is de­sir­able in bread (where elas­tic­ity traps yeast-pro­duced CO2 over long fer­men­ta­tion) and un­de­sir­able in pan­cakes (where it makes the in­te­rior tough and chewy). Three strate­gies min­i­mize gluten de­vel­op­ment:

Minimal mix­ing. Mechanical ac­tion aligns glutenin strands into or­ga­nized sheets. Organized gluten re­sists CO2 ex­pan­sion. The stan­dard in­struc­tion (“fold 10 – 12 strokes un­til just com­bined; lumps are de­sir­able”) is not a sug­ges­tion about aes­thet­ics; it is a struc­tural pre­scrip­tion. Overmixed bat­ter can lose 30% or more of its po­ten­tial vol­ume (McGee, 2004).

Fat as phys­i­cal bar­rier. Fat mol­e­cules bond to hy­dropho­bic amino acids along gluten pro­tein chains, phys­i­cally pre­vent­ing those chains from bond­ing to each other (McGee, 2004). This is the lit­eral mean­ing of shortening”: fat cre­ates a shorter, weaker gluten net­work. In a ri­cotta pan­cake, both the ri­cotta fat (10 – 13%) and the melted but­ter serve this func­tion. The cal­cu­la­tor tar­gets roughly 9% fat by bat­ter weight, which lands squarely among ac­claimed rich pan­cakes: work­ing from the pub­lished quan­ti­ties, a but­ter­milk-and-sour-cream bat­ter runs about 10% (López-Alt, 2015), a sour­dough-sponge bat­ter about 8% (NYT Cooking), and a sour-cream bat­ter as high as 13% (Perelman, 2009). The role mat­ters more than the source: the last bat­ter reaches the top of that range on sour cream alone, with no added but­ter at all, which is why the cal­cu­la­tor sizes but­ter only to fill what­ever fat the cho­sen dairy leaves short of the tar­get.

Protein sub­sti­tu­tion. By re­plac­ing much of the flour (and its gluten-form­ing pro­teins) with ri­cotta (which pro­vides struc­ture via pre-de­na­tured whey pro­teins that do not form gluten), the to­tal avail­able glutenin and gliadin is re­duced. The recipe drops from 165g flour (standard) to 125g flour (with ri­cotta): a 24% re­duc­tion in po­ten­tial gluten.

IV. Ricotta: pre-de­na­tured whey pro­teins

Ricotta” is Italian for recooked” (Latin re­co­quere: re- again” + co­quere to cook”). The name de­scribes the pro­duc­tion method: whey left over from pri­mary cheese­mak­ing is heated a sec­ond time to 80 – 93°C, de­na­tur­ing and ag­gre­gat­ing the whey pro­teins (primarily beta-lac­toglob­u­lin and al­pha-lac­tal­bu­min) that ca­sein-based cheeses leave be­hind (Journal of Dairy Science, 1988).

This pre-de­nat­u­ra­tion is the key in­sight. Beta-lactoglobulin un­der­goes ir­re­versible un­fold­ing and ag­gre­ga­tion above 78°C (ScienceDirect, 2015). Because ri­cotta has al­ready been heated to 80 – 93°C dur­ing pro­duc­tion, its pro­teins are fully de­na­tured be­fore they en­ter the pan­cake bat­ter. When the bat­ter is cooked again (internal tem­per­a­ture reach­ing ap­prox­i­mately 100°C), the ri­cotta pro­teins do not un­dergo fur­ther struc­tural change. They pro­vide a soft, cus­tard-like ma­trix with­out the tight­en­ing that oc­curs when raw pro­teins (like egg white) are de­na­tured for the first time.

Additionally, ri­cot­ta’s high mois­ture con­tent (around 74% wa­ter, within the 82.5% ceil­ing the USDA sets for ri­cotta) serves as a steam reser­voir dur­ing cook­ing, con­tribut­ing to the char­ac­ter­is­tic light­ness of the fi­nal pan­cake (USDA AMS).

V. The Maillard re­ac­tion and ex­te­rior brown­ing

Maillard brown­ing from a milk-solid-free pan fat: even color, lacy edge. The in­te­rior never ex­ceeded 100°C; this side did.

The golden-brown ex­te­rior of a pan­cake is pro­duced by the Maillard re­ac­tion: a non-en­zy­matic brown­ing re­ac­tion be­tween re­duc­ing sug­ars and amino acids that pro­duces melanoidins (brown pig­ments) and hun­dreds of volatile fla­vor com­pounds. It was first de­scribed by Louis-Camille Maillard in 1912 (Maillard, 1912), largely ig­nored un­til 1941, and for­mally sys­tem­atized by John Hodge in the most-cited pa­per in food sci­ence his­tory (Hodge, 1953; Finot, 2005).

The re­quire­ments for Maillard brown­ing on a pan­cake sur­face:

Surface tem­per­a­ture above 140°C. The in­te­rior of a pan­cake never ex­ceeds 100°C (limited by wa­ter’s boil­ing point), but the sur­face in con­tact with but­tered pan reaches 175 – 200°C (ThermoWorks). This tem­per­a­ture dif­fer­en­tial is why pan­cakes are brown out­side and pale in­side.

Reducing sug­ars. Sucrose (table sugar) in the bat­ter pro­vides glu­cose and fruc­tose upon in­ver­sion. Lactose from milk and ri­cotta is also a re­duc­ing sugar. Fructose be­gins carameliz­ing at 110°C (vs. 160°C for su­crose), so re­plac­ing some white sugar with honey (38% fruc­tose, 31% glu­cose) pro­vides more re­ac­tive Maillard fuel.

Amino acids. Provided by egg pro­teins, milk ca­sein, and whey pro­teins.

Low wa­ter ac­tiv­ity at the sur­face. A wet sur­face can­not ex­ceed 100°C. Surface de­hy­dra­tion is the rate-lim­it­ing step for crisp for­ma­tion. Clarified but­ter (smoke point 230°C vs. whole but­ter at 177°C) en­ables higher pan tem­per­a­tures, and its pure fat cre­ates more ef­fec­tive mi­cro-fry­ing zones that de­hy­drate the sur­face faster (Modernist Cuisine).

Cornstarch pro­duces crispier sur­faces than wheat flour be­cause its higher amy­lose con­tent (25 – 28% vs. wheat’s 20 – 22%) forms a rigid, porous, brit­tle net­work af­ter de­hy­dra­tion. The amy­lose mol­e­cules cross-link dur­ing heat­ing, cre­at­ing a struc­ture that frac­tures cleanly rather than bend­ing, and re­sists mois­ture re-ab­sorp­tion (America’s Test Kitchen; Cho et al., 2019). Mohamed et al. con­firmed this di­rectly: in model bat­ter sys­tems, crisp­ness cor­re­lated pos­i­tively with amy­lose con­tent and in­versely with oil ab­sorp­tion (Mohamed et al., 1998). Altunakar et al. tested corn starch, amy­lo­maize (70% amy­lose), waxy maize (0% amy­lose), and prege­la­tinized tapi­oca in chicken nugget bat­ters: corn starch pro­duced the high­est poros­ity, and all high-amy­lose starches sig­nif­i­cantly out­per­formed waxy maize for crisp­ness (Altunakar et al., 2004).

The cal­cu­la­tor adds no corn­starch, but it re­mains avail­able as a man­ual ex­per­i­ment. Frying bat­ters lean on it heav­ily: Korean fried chicken bat­ters rou­tinely use 50% corn­starch, ATKs fried chicken recipe uses 1:1, and com­mer­cial bat­ter patents spec­ify 50 – 80% high-amy­lose flour in the dry mix. Shih and Daigle found that high-amy­lose rice flour bat­ters re­duced oil up­take by up to 62%, though pure-starch coat­ings be­came more brit­tle (Shih & Daigle, 1999). Primo-Martín et al. showed that cross-linked starch (resistant to gela­tiniza­tion) fur­ther im­proved crisp­ness mea­sured by acoustic emis­sion, re­duc­ing oil con­tent from 28% to 20 – 23% (Primo-Martín et al., 2012). Two things ar­gue against it in a pan­cake. First, struc­ture: those bat­ters are coat­ings cling­ing to a sub­strate, while a free-stand­ing pan­cake needs gluten in­tegrity to flip with­out tear­ing, which is why a home ex­per­i­ment should stay un­der about 30% of the flour by weight. Second, fla­vor and tex­ture: amy­lose crisp is brit­tle and fla­vor­less, and past a small frac­tion it reads as an ar­ti­fi­cial, fried-coat­ing crunch rather than a pan­cake crust. So the de­pend­able, fla­vor­ful crisp is left to the Maillard crust and the lacy ghee-fried edges (a dry, open-pan fin­ish on a gen­er­ous fat), which carry both tex­ture and taste.

However, corn­starch also re­duces Maillard brown­ing: it con­tains 0.26% pro­tein com­pared to flour’s 10.3%, re­mov­ing roughly 40x the avail­able amino acids per gram re­placed. A crispy but pale pan­cake is a half-solved prob­lem. The cal­cu­la­tor com­pen­sates on both sides of the Maillard equa­tion: the sugar side and the amino acid side.

Not all sug­ars par­tic­i­pate equally in the Maillard re­ac­tion. Reducing sug­ars (those with a free alde­hyde or ke­tone group) re­act di­rectly with amino acids; su­crose, a non-re­duc­ing dis­ac­cha­ride, must first hy­drolyze into glu­cose and fruc­tose be­fore it can par­tic­i­pate. The brown­ing rate at pH 6 varies dra­mat­i­cally by sugar type (Buera et al., 1987):

Common sweet­ener sub­sti­tutes vary enor­mously in their Maillard po­ten­tial. Honey (81% re­duc­ing sug­ars by weight) is the clear win­ner. Molasses (24.7% re­duc­ing sug­ars plus cat­alytic iron and cop­per) has gen­uine brown­ing ben­e­fit but its strong fla­vor lim­its it to small doses. Brown sugar (2.5% re­duc­ing sug­ars from its ~3.5% mo­lasses coat­ing) and maple syrup (2.1% re­duc­ing sug­ars, 97% of its sugar is su­crose) are nearly iden­ti­cal to white sugar for brown­ing pur­poses; their ap­peal is fla­vor, not chem­istry (USDA FoodData Central).

Honey (approximately 38% fruc­tose, 31% glu­cose, 17% wa­ter) de­liv­ers 69% im­me­di­ately avail­able re­duc­ing sug­ars by weight, com­pared to 0% from white sugar un­til heat and acid cleave the gly­co­sidic bond. This makes honey a sub­stan­tially more ef­fec­tive Maillard fuel per gram of sweet­ener. However, this same re­ac­tiv­ity is the rea­son the cal­cu­la­tor does not use it: honey browns too ag­gres­sively at the pan tem­per­a­tures needed for proper in­te­rior cook­ing, nar­row­ing the for­give­ness win­dow be­tween golden and burnt to the point where con­sis­tent re­sults re­quire a PID-controlled cook­top. The cal­cu­la­tor uses white sugar and com­pen­sates for re­duced brown­ing via milk pow­der (concentrated ly­sine and lac­tose) on the amino acid side of the Maillard equa­tion.

The other Maillard re­ac­tant, amino acids, also varies dra­mat­i­cally across bat­ter in­gre­di­ents. Lysine is the most re­ac­tive amino acid in the Maillard re­ac­tion be­cause its side chain pro­vides an ep­silon-amino group in ad­di­tion to the al­pha-amino group pre­sent in all amino acids (Hemmler et al., 2018). Wheat flour con­tains only 285 mg ly­sine per 100g (lysine is wheat’s first lim­it­ing amino acid), while non­fat dry milk pow­der con­tains 2,720 mg per 100g. Even 10 – 15g of milk pow­der (about one ta­ble­spoon) adds 270 – 400 mg of ly­sine to the bat­ter, plus con­cen­trated lac­tose as an ad­di­tional re­duc­ing sugar. At high crisp set­tings where corn­starch dis­places flour pro­tein (and its al­ready-lim­ited amino acids), milk pow­der com­pen­sates on the amino acid side of the Maillard equa­tion.

This has an in­ter­est­ing im­pli­ca­tion for acid source se­lec­tion. Dairy acid sources (buttermilk, sour cream, Greek yo­gurt, ri­cotta) are dual-pur­pose: they pro­vide lac­tic acid for tang and CO2 pro­duc­tion, but the car­rier also brings lac­tose (a re­duc­ing sugar) and pro­tein (including ly­sine) that par­tic­i­pate di­rectly in Maillard brown­ing. Citrus juice pro­vides acid and noth­ing else for brown­ing. Fifty-five grams of sour cream con­tributes roughly 2g of pro­tein (~110 mg ly­sine) along­side its lac­tic acid; 30 mL of lemon juice con­tributes ~0.1g pro­tein and no re­duc­ing sug­ars. For max­i­mum brown­ing, at least one dairy acid source should be pre­sent. Citrus is not wasted in this con­fig­u­ra­tion: lemon zest pro­vides aro­matic ter­penes (limonene, cit­ral) that dairy can­not repli­cate, while the juice pro­vides ad­di­tional acid for tang. The ideal com­bi­na­tion for both brown­ing and fla­vor com­plex­ity is dairy acids plus cit­rus, not one or the other.

Salt (NaCl) has a bipha­sic re­la­tion­ship with the Maillard re­ac­tion that is of­ten mis­un­der­stood. At mod­er­ate con­cen­tra­tions (around 0.5% Na+, or roughly 1.25% NaCl), sodium ions ac­tu­ally pro­mote brown­ing: Luo et al. mea­sured 8.2x higher brown­ing in­ten­sity at 140°C com­pared to un­salted con­trols (Luo et al., 2019). The pro­posed mech­a­nism is that Na+ ions sta­bi­lize the tran­si­tion state of the con­den­sa­tion re­ac­tion be­tween car­bonyl and amino groups (Zhang et al., 2022). Only at very high con­cen­tra­tions (above ~6% NaCl) does the in­hibitory ef­fect dom­i­nate (Kwak & Lim, 2004). Typical sea­son­ing con­cen­tra­tions in bat­ters and on meat sur­faces (1 – 2% NaCl) fall in the pro­mot­ing range. Yamaguchi et al. fur­ther showed that NaCl does not sig­nif­i­cantly af­fect the brown­ing rate of glu­cose with pep­tides and pro­teins, only with free amino acids (Yamaguchi et al., 2009). Since the amino groups in a pan­cake bat­ter are mostly bound in in­tact pro­teins (flour gluten, egg al­bu­min, whey), the di­rect chem­i­cal ef­fect of salt on brown­ing at nor­mal sea­son­ing lev­els is likely neg­li­gi­ble.

Alkaline con­di­tions ac­cel­er­ate the Maillard re­ac­tion be­cause amino groups (RNH3+ at low pH be­come RNH2 at high pH) have in­creased nu­cle­ophilic­ity, mak­ing them more re­ac­tive with car­bonyl groups. This is why ex­cess bak­ing soda causes rapid brown­ing. J. Kenji López-Alt demon­strated this di­rectly by pho­tograph­ing iden­ti­cal bat­ters with in­creas­ing amounts of bak­ing soda: each in­cre­ment pro­duced vis­i­bly more brown­ing, up to the point where ex­cess un-neu­tral­ized soda pro­duced a soapy off-fla­vor (López-Alt, 2015).

This cre­ates a gen­uine trade­off be­tween tang and crisp that can­not be en­gi­neered away. Baking soda and acid are in the same liq­uid; they re­act spon­ta­neously on con­tact. You can­not add soda just for brown­ing” with­out it also neu­tral­iz­ing some of the resid­ual acid that pro­vides tang. The brown­ing ac­cel­er­a­tion re­quires rais­ing bat­ter pH, which means con­sum­ing hy­dro­gen ions, which means less sour­ness. The two goals are in di­rect chem­i­cal con­flict.

The cal­cu­la­tor re­solves this by keep­ing brown­ing en­tirely non-al­ka­line at all crisp lev­els: corn­starch (whose amy­lose net­work crisps in­de­pen­dently of pH), milk pow­der (concentrated ly­sine and lac­tose for the amino acid side of the Maillard re­ac­tion), and clar­i­fied but­ter or ghee (whose higher smoke point en­ables faster sur­face de­hy­dra­tion). These path­ways do not touch the acid bal­ance. You get max­i­mum crisp and max­i­mum tang si­mul­ta­ne­ously, no trade­off re­quired.

The pan tem­per­a­ture is com­puted from a ther­mal model that bal­ances two com­pet­ing timescales: the time for the cen­ter of the risen pan­cake to reach 95°C (egg pro­tein full co­ag­u­la­tion), and the time for the sur­face to reach tar­get brown­ing. The cen­ter time comes from the 1D slab heat equa­tion with mea­sured bat­ter ther­mal dif­fu­siv­ity (Baik et al., 1999; α = 1.3 × 10−7 m2/​s). The sur­face brown­ing rate fol­lows Arrhenius ki­net­ics with Ea = 64 kJ/​mol, mea­sured di­rectly on bread crust at 140 – 250°C (Zanoni et al., 1995). Honey mul­ti­plies the ef­fec­tive brown­ing rate by ap­prox­i­mately 1.7× (derived from the bak­ing guide­line of re­duc­ing oven tem­per­a­ture by 25°F for honey-sweet­ened goods, com­bined with the Zanoni ac­ti­va­tion en­ergy). Milk pow­der adds a fur­ther in­cre­ment via con­cen­trated ly­sine (27.2 mg/​g vs. 2.85 mg/​g in flour), the most Maillard-reactive amino acid. The cal­cu­la­tor solves for the pan tem­per­a­ture at which the sur­face reaches tar­get brown­ing ex­actly when the cen­ter is done, en­sur­ing even cook­ing with­out burn­ing.

Making Peace With Your Unlived Dreams

nik.art

I will never be a great snow­boarder. For var­i­ous ge­netic and non-ge­netic rea­sons, my knees are barely ca­pa­ble of sur­viv­ing a three-hour hike, let alone the land­ing af­ter a 1080.

In fact, I’ll prob­a­bly never be a snow­boarder at all, given my or­tho­pe­dist told me to stay away from any­thing that’s heavy on the knees, like ten­nis, ski­ing, or, say, snow­board­ing,” as long as 15 years ago. It sucks. I’d love to take snow­board­ing lessons. Alas, all I can do is watch videos of peo­ple do­ing sick stunts, liv­ing vic­ar­i­ously through GoPro’s Youtube chan­nel.

When I first found out, for a good while, I was re­ally up­set about this. How dare life take that from me!” I of­ten imag­ined what would hap­pen if I went big on snow­board­ing any­way. That there must be a way for me to fix my knees enough to suc­ceed, and, to be fair, there prob­a­bly is. But at some point, I re­al­ized that life is big but also short.

When asked What’s one ex­pe­ri­ence you hope we’ll share in the fu­ture?” ex-Bach­e­lor star Sharleen Joynt tells her hus­band: It’s hard. I want to do every­thing with you. There’s not enough time.”

You know what else I’d like to do be­sides be­com­ing a great snow­boarder? I want to learn kung fu. I’d also love to be a lot bet­ter at video games, get my Yu-Gi-Oh! hobby back on, and be­come at least flu­ent enough for every­day con­ver­sa­tion in oh, I don’t know, eight more lan­guages.

Meanwhile, back down on earth, I’m self-em­ployed. I spend most of my time work­ing, and when I don’t work, I try to be with my girl­friend, or fam­ily, or friends. It ebbs and flows, of course, but over the last few weeks, I’ve barely man­aged to make time to read, let alone pur­sue other, sec­ond-tier hob­bies.

Even if I won the lot­tery to­mor­row, how­ever, I doubt there’d be enough time. There’s never enough time. If Death ex­cused me for a few hun­dred years, I’d def­i­nitely take it.

And yet, some­how, the more years go by, the more rarely I watch snow­board­ing videos. My imag­i­na­tion runs wild less of­ten, and when it does, it comes with smiles more so than bit­ter­ness. It’s okay. Leave the snow­board­ing to oth­ers. You are a writer. You have things to do where you are, and that is enough.”

Use your imag­i­na­tion. Sometimes, dreams can just be dreams. They need­n’t all come true to feel sat­is­fy­ing. Watch videos. Read books. Spend time with the he­roes you’ll never meet. Whatever you do, don’t get an­gry at your un­lived dreams. Extend a hand. Make peace.

We only get to sam­ple a small taste of every­thing life has to of­fer, but in choos­ing de­lib­er­ately, we are do­ing the most im­por­tant job we were brought here to do.

Anti-social: It's fads, not friends, which now dominate our feeds

www.bbc.com

22 May 2026

John Laurenson

Getty Images

Social me­dia plat­forms used to be about com­mu­ni­ca­tion be­tween friends — now many are in­creas­ingly short video en­ter­tain­ment hubs. The busi­ness model is to in­crease the time peo­ple spend on their apps and in­crease ad rev­enue. But is there al­ready a con­sumer back­lash?

Aurélia fixes her­self a cof­fee, sits down in her beau­ti­ful gar­den not far from Paris and goes on Instagram to re­lax.” First up: a guy I like a lot who does in­te­rior de­sign. He’s in Venice at the mo­ment.” She’s into in­te­rior de­sign, and has even just had two bird draw­ings by the 19th Century English de­signer William Morris tat­tooed on her arms. She scrolls down. Two kit­tens hav­ing a fight. I love an­i­mals so I get a lot of an­i­mals. That’s how it works, so­cial me­dia. You click on ba­nanas and they give you ba­nanas.”

There are ads too — al­though they look just like the other posts — for a ro­bot-vac­uum cleaner, a diet and bed linen (with Morris-inspired de­signs). But no friends. She has 198 on Instagram but she says it’s com­pletely changed. I prac­ti­cally don’t see any friends’ posts any­more.” She’s pretty much given up post­ing her­self. I don’t think any­one sees them any­more any­way.”

While there re­main com­mit­ted so­cial, am­a­teur posters on Instagram and es­pe­cially Facebook, the switch from com­mu­ni­cat­ing with peo­ple you know to scrolling through pro­fes­sion­ally made con­tent from peo­ple you don’t, is even more pro­nounced among young users.

Kylian, 16, is in vo­ca­tional train­ing to be­come a chef. He’s on TikTok and Youtube a lot, he says. I like look­ing at videos more than pho­tos or mes­sages. I watch videos made by peo­ple I don’t know. I don’t post at all. I’m a rather shy per­son. I stay in my bub­ble. I watch and that’s all. I keep my re­ac­tions to my­self.”

I spend a lot of time scrolling through videos made by con­tent-cre­ators,” says Lucie, also 16. They’re more in­ter­est­ing than the posts of peo­ple I know.” She does­n’t post ex­cept some­times stories” which dis­ap­pear af­ter 24 hours.

Whether it’s TikTok, Snapchat, Facebook and Instagram, we are a long way from the digital town square” of per­sonal in­ter­ac­tion that so­cial me­dia was even just a few years ago.

John Laurenson

In France, an­nual of­fi­cial Barometre du nu­merique 2026 shows 49% of so­cial me­dia users are active only oc­ca­sion­ally”. In the UK, an Ofcom re­port  pub­lished in April showed a year-on-year drop of users who ac­tively post from 61% to 49%. In the US, a Morning Consult sur­vey of June last year found 28% re­ported post­ing less of­ten than the pre­vi­ous year. Just 33% now post daily com­pared to 57% who use it for en­ter­tain­ment daily. The gap is a lot wider still for Gen Z — 18% ac­tive for 74% pas­sive.

Vanessa Lalo, a Paris-based clin­i­cal psy­chol­o­gist spe­cial­is­ing in on-line be­hav­iour, says users have be­come more con­scious that the traces you leave (on so­cial me­dia) stay there for­ever and some no longer want to main­tain so­cial me­dia re­la­tions that can be su­per­fi­cial. Some don’t want the ex­po­sure to crit­i­cism that might be a risk when you post or the feel­ing that their post will seem poor along­side all the pro­fes­sional con­tent”.

However, Lalo adds, peo­ple haven’t stopped post­ing, rather they are post­ing dif­fer­ent things and in dif­fer­ent places. On TikTok, for ex­am­ple, young peo­ple pub­lish a lot of con­tent but it’s more funny par­o­dies and remixes of ex­ist­ing ma­te­r­ial. The goal is to make peo­ple laugh, not to tell peo­ple about their lives.”

That still hap­pens, she says, but it’s moved from so­cial me­dia plat­forms like Instagram and Facebook to mes­sag­ing sites like WhatsApp. There’s also been a move to­wards pri­vate groups on Instagram and Snapchat. These are much more in­ti­mate places where you’re not bom­barded with ads and con­tent made by in­flu­encers,” she says.

What we’re see­ing is so­cial me­dia split­ting in two,” says so­cial me­dia con­sul­tant Matt Navarra, au­thor of the Geekout Newsletter. Big plat­forms like Instagram and TikTok are be­com­ing more about en­ter­tain­ment and dis­cov­ery. WhatsApp is be­com­ing the place peo­ple go to ac­tu­ally be so­cial. The catch is, those kinds of spaces are harder for com­pa­nies to make money from.”

Small busi­ness own­ers are be­ing pushed to be­come pre­sen­ters, ed­i­tors, trend spot­ters and con­tent cre­ators, on top of ac­tu­ally run­ning the busi­ness — Matt Navarra

It was TikTok that helped to pi­o­neer an al­go­rithm that fig­ures out from the mo­ment you start scrolling what you like, and then fills your feed with ma­te­r­ial cal­cu­lated to keep you on the app for the longest pos­si­ble time.

Now, says Matt Navarra, Meta has built what it calls an AI sys­tem for un­con­nected con­tent rec­om­men­da­tions on Face­book and Instagram, which ba­si­cally means, they’re in­creas­ingly show­ing you stuff from peo­ple you don’t fol­low be­cause the ma­chine thinks you’re go­ing to like it. It’s not bi­ased to­wards, is it a pro­fes­sional cre­ator? Is it a brand? Is it a friend? If they can see that you’ve en­gaged with a friend a lot, you might see a lot more of their con­tent. It’s just that who you are friends with, who you fol­low, has be­come ir­rel­e­vant in a way.”

This all means that small busi­nesses, that have long used so­cial me­dia for free pro­mo­tion have to up their game.

There’s a real op­por­tu­nity for some small busi­nesses,” says Matt Navarra. A bak­ery, florist, sa­lon or lo­cal café can still break through if they have a good story, strong vi­su­als or be­hind-the-scenes con­tent peo­ple want to watch. But it also means the job has changed. Small busi­ness own­ers are be­ing pushed to be­come pre­sen­ters, ed­i­tors, trend spot­ters and con­tent cre­ators, on top of ac­tu­ally run­ning the busi­ness.”

The so­cial plat­forms con­tinue to be mon­e­tised pre­dom­i­nantly by ad rev­enue. That is still the core busi­ness model. And ad rev­enue con­tin­ues to grow — Matt Navarra

Social me­dia is evolv­ing into some­thing pas­sive like tele­vi­sion, al­beit tele­vi­sion that adapts as you zap. Or rather which knows you so well that it does­n’t seem to mat­ter that much that it’s taken the re­mote con­trol. You give the plat­form in­for­ma­tion about your­self that it uses for com­mer­cial gain and, in re­turn, it gives you con­tent tai­lored to please you for free.

The tran­si­tion from truly so­cial me­dia to en­ter­tain­ment plat­form does seem to be pay­ing off. The so­cial plat­forms con­tinue to be mon­e­tised pre­dom­i­nantly by ad rev­enue. That is still the core busi­ness model. And ad rev­enue con­tin­ues to grow,” says Matt Navarra. Global so­cial me­dia ad rev­enue is ex­pected to reach $317 bil­lion (£236bn) in 2026, up from $277 bil­lion (£206bn) last year. Meta is the biggest win­ner. Its ad sales al­ready in­creased 22% year-on-year in 2025. Ad sales are ex­pected to hit $243 bil­lion (£181bn) this year, enough to over­take Google for the first time.

AI pow­ered dig­i­tal ad tar­get­ing is be­com­ing ever more ef­fec­tive and pre­cise. The so­cial plat­forms al­low com­pa­nies to put ads amongst the con­tent that you’re scrolling through. Every third or fourth scroll is an ad. And they are the world’s best ad tar­get­ing en­gines. They know so much about your in­ter­ests be­cause of what you’ve looked at, liked, en­gaged with, what you’ve cho­sen to fol­low, the time you’ve spent in cer­tain ar­eas of the app, things like that,” Navarra says.

So ad­ver­tis­ers will go in and say: I want to place an ad next to peo­ple in the UK who are be­tween thirty and sixty years old and who are in­ter­ested in DIY and the so­cial plat­forms will have that in­for­ma­tion and will place the ads ac­cord­ingly.”

The price will de­pend on the num­ber of im­pres­sions (clicks) the ad­ver­tiser wants and how tight the cri­te­ria are. It costs more to place ads in the so­cial me­dia feeds of peo­ple who buy horses than peo­ple who buy ice-cream.

More like this:

Might there be a back­lash com­ing? Don’t many peo­ple go on to so­cial me­dia to see how friends are re­act­ing to their posts or com­ments be­fore set­tling down to scroll through pro­fes­sion­ally made con­tent?

Meanwhile, for those who miss what are fast be­com­ing the old days when so­cial me­dia en­abled you to share a bit of your life, a joke or a point of view with peo­ple you more-or-less knew, there are tools within plat­forms, says Matt Navarra, that al­low you to choose to see mainly friends and fam­ily con­tent. People can flick to a feed that gives them that,” he says. But most peo­ple don’t.”

For more on busi­ness and be­yond, fol­low us on LinkedIn.

How much of Thermo Fisher’s antibody data has been manipulated?

reeserichardson.blog

[ TL;DR: As of 3 June 2026, we have iden­ti­fied more than 450 im­ages bear­ing signs of ma­nip­u­la­tion in ver­i­fi­ca­tion data ad­ver­tised by Thermo Fisher Scientific in its on­line pri­mary an­ti­bod­ies cat­a­log (+1 by Abcam). See the full repos­i­tory of prob­lem­atic im­ages, cu­rated by my­self and Sholto David, here:

Zenodo — Problematic im­ages in ven­dor an­ti­body ver­i­fi­ca­tion data

You are wel­come to con­tribute new find­ings at this Google form.

This blog post was orig­i­nal posted on 28 May 2026 and has not been edited to up­date counts since that date. There is an up­date cov­er­ing Thermo Fisher’s re­sponse at the bot­tom of this post. ]

A week and a half ago, while look­ing for trust­wor­thy data demon­strat­ing a cell line’s de­fi­ciency in the pro­tein p53, Sholto David came across the fol­low­ing im­age of a Western blot in Thermo Fisher Scientific’s on­line an­ti­bod­ies cat­a­log:

This im­age is sup­posed to demon­strate that the an­ti­body be­ing sold works as in­tended. It is la­beled as Advanced Verification” data on Thermo Fisher’s site and its cap­tion im­plies that the data was pro­duced in­ter­nally (other im­ages in the cat­a­log that have not been pro­duced in­ter­nally are la­beled un­der Published Figures”).

This Western blot ap­pears to be fab­ri­cated. As an­no­tated by Sholto, sev­eral of the bands in the im­age are iden­ti­cal af­ter flip­ping and ro­ta­tion:

Shortly af­ter, Johan Duchêne no­ticed a sim­i­larly sus­pi­cious im­age of an­other anti-p53 an­ti­body in Thermo Fisher’s cat­a­log. I de­cided to go look­ing my­self and quickly turned up ten more sus­pi­cious im­ages on eight other an­ti­body prod­ucts of­fered by Thermo Fisher.

Sholto and I have now doc­u­mented more than 100 im­ages pro­vided as ver­i­fi­ca­tion data in Thermo Fisher’s an­ti­body cat­a­log that have ap­par­ently been ma­nip­u­lated. You can see all of them at this Zenodo repos­i­tory, which we’ll try to up­date reg­u­larly. This repos­i­tory also con­tains a hand­ful of in­stances that are less sug­ges­tive of ma­nip­u­la­tion, but the data is still prob­lem­atic (e.g., the same im­age be­ing pre­sented as ver­i­fi­ca­tion data for two dif­fer­ent an­ti­bod­ies).

Here are some high­lights:

Some im­ages are sim­i­lar to the ex­am­ple that started this ex­cur­sion and also fea­ture bands that are un­usu­ally sim­i­lar to one an­other.

Many im­ages, if you ad­just the con­trast, fea­ture con­spic­u­ous brushstrokes”, sug­gest­ing that part of the im­age has been painted over in a pro­gram like Photoshop.

Other im­ages fea­ture repet­i­tive blocks of back­ground noise, sug­gest­ing that parts of the im­age were copy-pasted over each other. They might also fea­ture sud­den un­ex­pected dis­con­ti­nu­ities in the pat­tern of back­ground noise.

In one in­stance, I thought I had stum­bled across an­other one of these in­stances of du­pli­cated blocks of back­ground noise…

…only to dis­cover that dozens of an­ti­bod­ies for sale from Thermo Fisher pre­sent a ver­i­fi­ca­tion Western blot that fea­tures this ex­act back­ground pat­tern, just with min­i­mal ed­its such that the sin­gle band is po­si­tioned where one would ex­pect to see the pro­tein of in­ter­est.

At the time of writ­ing, we’ve doc­u­mented 50 in­stances of this back­ground pat­tern ap­pear­ing in ver­i­fi­ca­tion data on Thermo Fisher’s site, but this is far from an ex­haus­tive list. Similar im­age” searches us­ing Google Lens, Bing Images or DuckDuckGo be­tray hun­dreds more that we have yet to doc­u­ment.

Antibodies are near-ubiq­ui­tous but no­to­ri­ously fickle lab­o­ra­tory reagents in bio­med­ical re­search. For many ap­pli­ca­tions, it is ab­solutely cru­cial that the an­ti­bod­ies that you use are se­lec­tive (i.e., the an­ti­body binds strongly to the tar­get pro­tein) and spe­cific (i.e., the an­ti­body binds to the pro­tein of in­ter­est and lit­tle else). Commercially-available an­ti­bod­ies of­ten fail to meet these cri­te­ria. Members of YCharOS, an in­de­pen­dent an­ti­body val­i­da­tion ini­tia­tive, es­ti­mated in 2024 that more than 50% of all an­ti­bod­ies failed in one or more ap­pli­ca­tions”. Antibodies that don’t work as in­tended can de­lay ex­per­i­ments by weeks and non-spe­cific an­ti­bod­ies are a mas­sive source of ir­re­pro­ducibil­ity in the bio­med­ical lit­er­a­ture. To learn more, check out Johan’s September 2025 talk in which he de­tails his ex­pe­ri­ence with a study pub­lished us­ing a non-spe­cific an­ti­body.

Antibody ven­dors like Thermo Fisher (probably the largest lab­o­ra­tory reagent sup­plier in the world) put ver­i­fi­ca­tion data in their cat­a­logs to demon­strate to sci­en­tists that the prod­uct works as in­tended. While signs of ma­nip­u­la­tion in this ver­i­fi­ca­tion data don’t nec­es­sar­ily im­ply that the an­ti­bod­ies in ques­tion don’t work as ad­ver­tised, with­out re­li­able ver­i­fi­ca­tion data avail­able, sci­en­tists will have no way of know­ing un­til they have ac­tu­ally pur­chased the an­ti­body. And an­ti­bod­ies are not cheap; at Thermo Fisher, a sin­gle vial con­tain­ing a 0.1 mL aliquot of an­ti­body so­lu­tion typ­i­cally costs 400 to 500 USD.

We cre­ated our repos­i­tory of prob­lem­atic im­ages in ven­dor an­ti­body cat­a­logs A) to raise aware­ness among work­ing bio­med­ical sci­en­tists that the an­ti­body ver­i­fi­ca­tion data they see in a ven­dor’s cat­a­log may be un­re­li­able and B) to en­cour­age oth­ers to look for and re­port prob­lem­atic ven­dor-pro­vided an­ti­body ver­i­fi­ca­tion data (not lim­ited to just Thermo Fisher). If you spot any­thing, feel free to fill out this Google form so that it might be added to the spread­sheet and repos­i­tory.

A part­ing mes­sage: al­ways val­i­date your an­ti­bod­ies!

UPDATE 8 June 2026: Thermo Fisher has re­leased a galling 15-point re­sponse to our ob­ser­va­tions. The most im­por­tant part (in my as­sess­ment) is quoted be­low (emphasis mine):

6. Did Thermo Fisher ma­nip­u­late or fab­ri­cate an­ti­body data?No. The Company fully stands by the data and un­der­ly­ing sci­ence. At Thermo Fisher Scientific, as the world leader in serv­ing sci­ence, sci­en­tific in­tegrity is a core value. The Company takes an­ti­body val­i­da­tion, speci­ficity and ac­cu­rate prod­uct doc­u­men­ta­tion se­ri­ously, and is com­mit­ted to the trans­par­ent and eth­i­cal gen­er­a­tion, analy­sis and pre­sen­ta­tion of sci­en­tific data. In the process of prepar­ing an­ti­body im­ages for pub­li­ca­tion on its web­site, some im­ages may have been ad­justed to clar­ify for pre­sen­ta­tion pur­poses — not to al­ter or mis­rep­re­sent the un­der­ly­ing ex­per­i­men­tal re­sults. Thermo Fisher rec­og­nizes, how­ever, that im­age ad­just­ments of any kind can raise ques­tions about data in­tegrity, which is why mov­ing for­ward, where an orig­i­nal im­age is not pre­sent or avail­able, the Company will en­sure that web­site users are in­formed that an­ti­body im­ages may have been op­ti­mized for pre­sen­ta­tion and clar­ity on the web­site.

6. Did Thermo Fisher ma­nip­u­late or fab­ri­cate an­ti­body data?

No. The Company fully stands by the data and un­der­ly­ing sci­ence. At Thermo Fisher Scientific, as the world leader in serv­ing sci­ence, sci­en­tific in­tegrity is a core value. The Company takes an­ti­body val­i­da­tion, speci­ficity and ac­cu­rate prod­uct doc­u­men­ta­tion se­ri­ously, and is com­mit­ted to the trans­par­ent and eth­i­cal gen­er­a­tion, analy­sis and pre­sen­ta­tion of sci­en­tific data. In the process of prepar­ing an­ti­body im­ages for pub­li­ca­tion on its web­site, some im­ages may have been ad­justed to clar­ify for pre­sen­ta­tion pur­poses — not to al­ter or mis­rep­re­sent the un­der­ly­ing ex­per­i­men­tal re­sults. Thermo Fisher rec­og­nizes, how­ever, that im­age ad­just­ments of any kind can raise ques­tions about data in­tegrity, which is why mov­ing for­ward, where an orig­i­nal im­age is not pre­sent or avail­able, the Company will en­sure that web­site users are in­formed that an­ti­body im­ages may have been op­ti­mized for pre­sen­ta­tion and clar­ity on the web­site.

The phrase antibody im­ages may have been op­ti­mized for pre­sen­ta­tion and clar­ity on the web­site” is re­peated on this FAQ page six times. I en­cour­age read­ers to pe­ruse the im­ages col­lected in our Zenodo repos­i­tory and de­cide what could and could not char­i­ta­bly be de­scribed as optimization for pre­sen­ta­tion and clar­ity”.

The Smallest Brain You Can Build

ranpara.net

A per­cep­tron is the small­est brain you can build. One num­ber goes in. One yes-or-no an­swer comes out. That is the whole thing.

It sounds too sim­ple to mat­ter. But this tiny idea is the seed of every neural net­work run­ning to­day. In this post we build a per­cep­tron from scratch in Python, and we watch it learn, live, in your browser. No heavy math. No big li­braries. Just a weight, a bias, and a loop.

I am not a na­tive English speaker, and I am still learn­ing this field my­self. So I will ex­plain it the way I needed some­one to ex­plain it to me. Slowly, and from the ground up.

What is a per­cep­tron?

In 1958, a re­searcher named Frank Rosenblatt built a ma­chine he called the per­cep­tron.

It was in­spired by a sin­gle brain cell, a neu­ron. A neu­ron takes in sig­nals, and if those sig­nals are strong enough, it fires. Rosenblatt copied that idea in math:

out­put = 1 if (w · x + b) > 0 0 oth­er­wise

Here x is the in­put, w is the weight, and b is the bias. Do not worry about those words yet. We will meet each of them by build­ing some­thing real.

Think like a hu­man first

Before a ma­chine de­cides any­thing, let us watch a hu­man de­cide. Meet John Doe. He has a job of­fer, and he must an­swer one ques­tion: should he take it?

John does not flip a coin. He weighs things. Some fac­tors mat­ter to him more than oth­ers.

John mul­ti­plies each fac­tor by how much he cares about it, then adds every­thing up. If the to­tal is high enough, he says yes. If not, he says no.

That is a per­cep­tron. The fac­tors are the in­puts. How much he cares is the weight. And high enough” is a thresh­old he car­ries in his head. Hold on to that thresh­old. Later we will give it a name: the bias.

The sim­plest pos­si­ble de­ci­sion: is this num­ber pos­i­tive?

Let us shrink the prob­lem un­til al­most noth­ing is left. One in­put. One ques­tion.

Is this num­ber pos­i­tive?

Is this num­ber pos­i­tive?

That is it. Feed the ma­chine a num­ber. It should an­swer True for pos­i­tive and False for neg­a­tive.

The ma­chine makes its guess like this:

pre­dic­tion = (weight * value + bias) > 0

Multiply the in­put by the weight, add the bias, and check if the re­sult is above zero. If yes, it pre­dicts True. If no, it pre­dicts False. This lit­tle for­mula is the clas­si­fier, also called the de­ci­sion func­tion.

At the start, the weight and bias are just ran­dom num­bers. So the ma­chine guesses badly. Now comes the only clever part: it learns from its mis­takes.

if pre­dic­tion != re­sult: er­ror = re­sult - pre­dic­tion # True - False = 1, False - True = -1 weight += learn­ing_rate * er­ror * value bias += learn­ing_rate * er­ror

When the guess is wrong, we nudge the weight and bias in the right di­rec­tion. The er­ror tells us which way to nudge. The learn­ing rate de­cides how big each nudge is. We do this for every ex­am­ple, then re­peat the whole pass again. One full pass over the data is called an epoch. Repeating epochs is train­ing.

Here is that ex­act ma­chine. Press Train and watch it learn. Each green dot is a pos­i­tive num­ber (True), each red dot is neg­a­tive (False), and the blue dashed line is where it has de­cided to split them.

epoch 0 weight 0 bias 0 bound­ary – ac­cu­racy 0%

It snaps into place al­most im­me­di­ately. Look at the read­out: the bound­ary lands right around 0, and the bias set­tles near 0 too.

That is not an ac­ci­dent. For this prob­lem, we never needed the bias at all. Which is strange, be­cause bias is sup­posed to be im­por­tant. To see why it mat­ters, we need a harder ques­tion.

What is a de­ci­sion bound­ary?

That blue line has a name: the de­ci­sion bound­ary. It is the ex­act point where the ma­chine flips from say­ing False to say­ing True.

We can com­pute it. The bound­ary sits where w · x + b = 0. Solve for x:

de­ci­sion_bound­ary = -bias / weight

For is this num­ber pos­i­tive,” the bound­ary should be at 0. And it is. Now watch what hap­pens when the right an­swer is not at zero.

Why do we need bias? The stu­dent-pass ex­am­ple

New prob­lem. Same ma­chine. We give it exam scores from 0 to 100, and we ask:

Did the stu­dent pass?

Did the stu­dent pass?

The rule is sim­ple: a score of 50 or higher passes. So the de­ci­sion bound­ary should sit at 50, not at 0.

Let us try to solve it the way we solved the last one, us­ing the weight only. In the demo be­low, turn off Use bias” and press Train.

epoch 0 weight 0 bias 0 bound­ary – ac­cu­racy 0%

Use bias

Watch the ac­cu­racy. It climbs to around 50 per­cent and then gets stuck. It can­not do bet­ter, no mat­ter how long you train it.

Here is why. With no bias, the for­mula is just weight * score. Every exam score is a pos­i­tive num­ber. So if the weight is pos­i­tive, the ma­chine calls every stu­dent a pass. If the weight is neg­a­tive, it fails every­one. The bound­ary is glued to 0, and it can­not move. A line forced through zero sim­ply can­not sep­a­rate below 50” from 50 and up.”

Now turn Use bias” back on and press Train again. The ac­cu­racy climbs all the way to 100 per­cent, and the bound­ary slides over and parks near 50.

That is the whole job of the bias. The weight sets the steep­ness. The bias moves the bound­ary left or right so it can sit wher­ever the an­swer ac­tu­ally is. Remember de­ci­sion_bound­ary = -bias / weight. With a bias, the bound­ary can be any­thing. Without one, it is stuck at zero for­ever.

The one sen­tence to re­mem­ber: when your in­puts sit far from zero, you need a bias to move the line to them.

How does a per­cep­tron learn? Epochs and learn­ing rate

You saw two di­als while train­ing: epochs and learn­ing rate.

An epoch is one full pass over all the data. The ma­chine rarely gets every­thing right in a sin­gle pass, so we go again, and again. More epochs means more chances to fix mis­takes. That is why ac­cu­racy climbs as you keep train­ing.

The learn­ing rate is the size of each cor­rec­tion. In the code it is the learn­ing_rate mul­ti­plier:

weight += learn­ing_rate * er­ror * value

Small steps are care­ful but slow. Big steps are fast but can over­shoot and bounce around. Choosing it well is part of the craft. Here we used 0.1, which is gen­tle enough to stay sta­ble.

Why do we nor­mal­ize data?

There is a quiet prob­lem hid­ing in the pass ex­am­ple. Look at that up­date line again:

weight += learn­ing_rate * er­ror * value

The cor­rec­tion is mul­ti­plied by value. For exam scores, value can be as large as 100. So a sin­gle wrong guess can throw the weight by a huge amount. The ma­chine still learns, but it lurches around in­stead of set­tling smoothly.

The fix is nor­mal­iza­tion: shrink the in­puts to a small, tidy range be­fore train­ing. The sim­plest ver­sion is to di­vide every score by the largest pos­si­ble score, so 0 to 100 be­comes 0 to 1.

In the demo be­low, first press Train with nor­mal­iza­tion off and watch the ac­cu­racy line jump around on its way up. Then turn Normalize data” on, re­set, and train again. Same ma­chine, same an­swer, but it gets there in a frac­tion of the epochs, and the climb is smooth.

ac­cu­racy per epoch

epoch 0 weight 0 bias 0 bound­ary – ac­cu­racy 0%

Normalize data

One hon­est note. With a sin­gle in­put like this, nor­mal­iza­tion mostly buys you speed and calm. It be­comes es­sen­tial when your in­puts live on very dif­fer­ent scales. Think back to John Doe: his pay was mea­sured in thou­sands of dol­lars, but same city” was just a 0 or a 1. Without nor­mal­iza­tion, the dol­lars would drown out every­thing else, and the ma­chine would ba­si­cally ig­nore the city. Putting both on the same scale lets each fac­tor get a fair say. (Dividing by the max is the easy ver­sion; a com­mon gen­eral method is to sub­tract the mean and di­vide by the spread, called stan­dard­iza­tion.)

The full per­cep­tron in Python

Here is the com­plete pro­gram for is this num­ber pos­i­tive,” with noth­ing hid­den. It is short enough to read in one sit­ting.

im­port ran­dom

learn­ing_rate = 0.1 EPOCHS = 100

weight = ran­dom.uni­form(-1, 1) bias = ran­dom.uni­form(-1, 1)

# pos­i­tive num­bers are True, neg­a­tive num­bers are False data = [(i * 0.1, True) for i in range(1, 501)] data += [(i * 0.1, False) for i in range(-500, 0)] ran­dom.shuf­fle(data)

for epoch in range(EPOCHS): for value, re­sult in data: pre­dic­tion = (weight * value + bias) > 0 if pre­dic­tion != re­sult: er­ror = re­sult - pre­dic­tion # +1 or -1 weight += learn­ing_rate * er­ror * value bias += learn­ing_rate * er­ror

de­ci­sion_bound­ary = -bias / weight print(f”weight = {weight:.3f}“) print(f”bias = {bias:.3f}“) print(f”de­ci­sion bound­ary = {decision_boundary:.3f}“)

To turn this into the stu­dent-pass ma­chine, you change two things: make the data exam scores with re­sult = score >= 50, and, if you want to feel the pain of a miss­ing bias, freeze the bias at 0. Everything else stays the same.

Acknowledgments

The core in­spi­ra­tion for this post came from the fan­tas­tic video ChatGPT is made from 100 mil­lion of these [The Perceptron] by Welch Labs. If you are a vi­sual learner and want to see the rich his­tory and hard­ware be­hind these con­cepts, I highly rec­om­mend watch­ing it!

What’s next?

You just built a work­ing per­cep­tron. It takes an in­put, weighs it, adds a bias, and de­cides. It learns from its own mis­takes, one epoch at a time.

A sin­gle neu­ron can only draw one straight line. The magic starts when you stack them: the out­put of one neu­ron be­comes the in­put of the next. Layer enough of them to­gether and you get a neural net­work that can learn shapes far more tan­gled than a sin­gle line. But every one of those neu­rons is do­ing ex­actly what you just watched. A weight, a bias, a de­ci­sion.

If you want the non-tech­ni­cal story of how I ended up writ­ing code in Canada at all, I wrote about it here: The Outsider Who Shipped Anyway.

Thanks for build­ing this with me. Now go change the num­bers and break it. That is the fastest way to learn.

AI-native React Components

vorpus.github.io

To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".

10HN is also available as an iOS App

If you visit 10HN only rarely, check out the the best articles from the past week.

Visit pancik.com for more.