The Life of a Headline

A field study in 15 plates

The Life of a Headline

Every morning at six a program reads ninety news headlines, decides which ones are worth writing about, writes a briefing, scores the state of the world, and clears a cache — all so one person can glance at it over coffee. This is one article’s passage through it, drawn plate by plate and checked against the source.

Read the study7 min
The Life ofa Headlinea field study in 15 plates18 SEPTEMBER 2026GOOGLE NEWSOne headline, followed from the wire to the reader.I
  1. 01Title
  2. 02The Wire
  3. 03Emission
  4. 04The Collector
  5. 05Retrieval
  6. 06Sorting
  7. 07Anatomy
  8. 08Submersion
  9. 09Condensation
  10. 10The Assay
  11. 11Cartography
  12. 12Deposition
  13. 13The Signal
  14. 14Distribution
  15. 15The Reader

The film · 75 seconds

Fifteen plates, five seconds each, drawn in order. The study below takes the same fifteen at reading pace.

The Life ofa Headlinea field study in 15 plates18 SEPTEMBER 2026GOOGLE NEWSOne headline, followed from the wire to the reader.I
0.0s / 75s
Plate I · Titlespace to play · ← → for plates

Plate I · Title

One morning, one headline

Every morning at six, a program wakes up, reads about ninety news headlines, decides which ones are worth writing about, writes a briefing, scores the state of the world on a scale from war to calm, and clears a cache so that one reader can look at the result over coffee. Start to finish it takes about three minutes.

This is a study of one article's passage through that program. Not the system in the abstract — one specific item, followed from the moment it appeared on a wire to the moment it appeared on a phone, with every stage drawn and every number checked against the source.

The thing worth knowing in advance: almost nothing here is clever. The interesting parts are the decisions somebody didn't make.

15
stages
~3 min
of machine time

Plate II · The Wire

Eight feeds, ninety headlines

The pipeline doesn't subscribe to a wire service. It reads eight Google News RSS queries — one per section of the briefing: global, europe, ai, tech, science, markets, poland, wroclaw — each capped at ten or twelve items and each restricted to the last twenty-four hours.

Ninety headlines is the entire extent of what the system knows about the day. Every stage after this one is a transformation of those ninety lines of text. Nothing else gets in.

The outlet names you'd expect — Reuters, PAP, TVN24 — do turn up, but as metadata on individual items rather than as sources anybody chose. The system is reading a search engine's idea of the news, not a newsroom's, and that is a different thing in ways nobody downstream can detect.

8
feeds
90
headlines, at most

lambda_function.py — the headlines dict

Plate III · Emission

What an item actually is

A feed item, as the parser keeps it, is four fields: a title, a link, an outlet name, and a publication timestamp — plus a description truncated at a hundred and sixty characters. That is the whole record. Everything else in the XML is read and dropped.

The timestamp is the one field that matters more than it looks. It marks the only moment in this entire sequence that happens outside the system, and the last plate measures from it. Everything between is machinery.

4
fields kept
160
characters of dek

lambda_function.py — fetch_rss()

Plate IV · The Collector

Six o'clock, three times

An EventBridge rule fires at 06:00 UTC and invokes a Lambda. Two more rules fire at 06:10 and 06:30. That isn't redundancy by accident — it's a retry strategy with no coordination between the attempts. Each invocation checks whether today's edition already exists and returns immediately if it does.

So the six o'clock run does the work and the other two cost a few milliseconds each. It's a cruder mechanism than a proper retry with backoff, and it is exactly as reliable for a job that runs once a day.

The function is cold when it starts: no warm container, no cached state, 512MB of memory, and a fifteen-minute ceiling. The ceiling used to be ten, and the reason it is fifteen is that one morning the run hit it. That is plate VIII's story.

06:00
UTC, daily
900 s
timeout

infra/template.yaml — DailyAINewsTrigger; the backend README on the timeout-budget incident

Plate V · Retrieval

Eight requests, one after another

The eight feeds are fetched sequentially, not in parallel. They're built in a Python dict literal, which evaluates its values in order, so each fetch waits for the one before it to return. Each gets an eight-second timeout and none of them uses it.

Running them concurrently would save perhaps five seconds on a job that takes three minutes and runs once a day. It has never been worth the change — which is the correct engineering answer, and an unusually easy one to say out loud.

8 s
timeout, each
~5 s
total, in practice

lambda_function.py — the handler

Plate VI · Sorting

Eighty-nine get read and not written

There is no deduplication step here, no clustering, no relevance score. All ninety headlines go into a single prompt and the model decides what to write about. The ones it passes over aren't discarded as redundant — they're simply not chosen.

That distinction matters. A deduplication stage would be a claim that the discarded items said the same thing. What actually happens is an editorial judgement made by a language model against a system prompt, once, with no record of why.

Our specimen is one of the ones it kept. From this plate to the last it is the only thing on any sheet in vermilion, because from here on there is a specimen to follow.

1
of ninety
0
dedup stages

lambda_function.py — call_gemini_edition()

Plate VII · Anatomy

The article is never fetched

This is the fact that changes how you read every stage after it. The pipeline stores the article's URL and never follows it. No downstream step ever sees the article.

So the model writes from a headline and a hundred-and-sixty-character description. That is the entire input for a story that will run to a hundred words — and the system prompt is emphatic about the consequence: summarize only what the provided headlines state, never invent numbers, quotes, names, dates, causes or consequences.

It's a strict rule because with that little input the alternative isn't paraphrase, it's confabulation. Whether an instruction is sufficient protection against that is the open question the whole design rests on.

0
articles fetched
1
instruction standing in the way

lambda_function.py — SYSTEM_PROMPT

Plate VIII · Submersion

Ninety items, one prompt

All ninety headlines and their deks, yesterday's stability scores, and a set of Wikipedia 'on this day' events go to the model in a single call, with a JSON schema constraining the shape of what comes back.

Our specimen is a little over a thousand tokens of that prompt — a fraction of a percent of the context it sits in. It is not being read closely. It's being read alongside eighty-nine others.

The call has a two-hundred-second per-attempt timeout and a four-hundred-and-twenty-second budget across the whole fallback chain: three Gemini models, then Groq. Those numbers are scar tissue. An earlier version had a per-attempt timeout and no overall budget, spent all of it on one hung model, and hit the Lambda's own ceiling before it could send the alert telling anyone.

420 s
total budget
4
models in the chain

GenerateAINewsSummary/README.md — timeout budget design

Plate IX · Condensation

Where the article becomes a briefing

This is the transformation the whole pipeline exists to perform. A headline and a dek go in; a written, attributed, grounded story comes out, considerably shorter than the sum of what the model was shown.

Everything before this plate moves the article around. Everything after it moves the result around. It is the only stage a person couldn't do faster by hand, and the only stage whose output isn't a rearrangement of its input.

It is also the only stage that can be wrong in a way nothing downstream can detect. A failed fetch throws. A failed write throws. A plausible sentence that isn't supported by its source looks exactly like a good one.

1
irreversible step

lambda_function.py — EDITION_SCHEMA

Plate X · The Assay

One number for the whole morning

Along with the stories, the model returns a single integer from −10 (war, chaos) to +10 (peace, calm), judged only from the headlines it was handed and anchored against yesterday's figure so the series doesn't lurch. It returns a short clause naming the headline that moved it, too.

It is a made-up number in the sense that no instrument measured anything. It is not a made-up number in the sense that it is bounded, reproducible, anchored to the previous day, and traceable to specific named inputs. Most published indices manage one of those and not the other.

−10 … +10
chaos to calm

lambda_function.py — stability_score

Plate XI · Cartography

Eight regions, scored every day

The same call scores eight regions from 0 (calm) to 10 (crisis), each with a clause naming what drove it. The scores are appended to a running series, so the briefing has a memory: today's figure is anchored to yesterday's, which was anchored to the day before.

The map is deliberately not a projection. The underlying data is one integer per region per day; drawing it on an accurate coastline would invite a reader to read a border off it, and the data cannot support that reading.

8
regions
364
days of history kept

lambda_function.py — REGION_KEYS, update_history()

Plate XII · Deposition

The whole edition, in one row

The briefing is written to DynamoDB as a single item under one partition key: id = LATEST, a date, and the entire edition serialised into a content field as JSON — eight sections, the cross-stream Signal, the back-page games, the featured paper.

There is no sort key. That is a design decision, not an omission: one read serves the whole page, there are no joins, and the only constraint that has ever bitten is DynamoDB's 400KB item ceiling. The archive of past editions is trimmed to stay under it, and currently holds about twenty-one.

The same run writes half a dozen more items — the archive, the score history, the local pulse, the games, the rolling chronicle. The edition is the one anybody reads.

1
item, one key
400 KB
ceiling

infra/template.yaml — AINewsSummaryTable

Plate XIII · The Signal

Telling the cache to let go

The Lambda POSTs to the frontend with a shared secret. Next.js drops its cached copies of /news, /reel, /play and /play/[game]. The round trip is a few hundred milliseconds.

Without it the edition still appears — but on a three-hundred-second revalidation timer, and because of stale-while-revalidate, only on the reload after that one. That is the difference between a request and a timeout, three orders of magnitude apart, and it is the entire reason this endpoint exists.

It was written to fix a specific complaint: the site showed yesterday's news until you hard-refreshed.

~300 ms
round trip
300 s
the alternative

app/api/revalidate/route.ts

Plate XIV · Distribution

Everywhere, in about a second

The invalidation propagates to the CDN's edge locations. From the origin in us-east-1 the wave reaches the furthest node in roughly a second.

This is the least interesting stage in the pipeline and by some distance the most reliable. It is also the only one nobody built. It is what you get for deploying to a platform, and it would be the hardest stage here to reproduce yourself.

~1 s
to the far edge

Plate XV · The Reader

And then somebody glances at it

The headline arrives on a phone. Total elapsed time from the moment the item appeared on the wire to the moment the cache was clear is about an hour and fifty minutes — almost all of it spent waiting for six o'clock.

The pipeline's own share is roughly three minutes. Everything in this study — the eight feeds, the trap, the ninety slips, the column of sediment, the needle, the map, the strata, the pulse — resolves into something a person reads in four seconds and does not think about again.

That isn't a complaint. That is what infrastructure is: the measure of it working is that none of it shows.

4 s
of reading