Who Texted Whom, When: What a Phone Bill Reveals Without Content
Nobody reading your phone bill can see what you said. They can still work out a surprising amount: who matters to you, who you contact at 2am, where you sleep, and when something in your life changed. Content is the part everyone worries about; the pattern is the part that tells the story.
Let's prove it with a few lines of code and a made-up log, then look at what UK law calls this kind of data.
What is actually in the record
A call or message record is basically a row. Exact fields vary by provider and by what is being asked for, but the common shape is:
- the number that originated the event and the number it went to
- a timestamp, and for calls a duration
- the type of event (call, SMS, data session)
- the cell or mast the handset was using at the time
No message text, no audio. This is why it is often called "just metadata", which is a phrase doing a lot of quiet work.
A quick experiment on a fake log
Here is a tiny Go program over invented records. The numbers come from the 07700 900 range, which Ofcom reserves for drama use, so nobody real is involved. It counts events per contact and how many fall between 23:00 and 05:00.
package main
import (
"fmt"
"sort"
"time"
)
type Record struct {
When time.Time
To string
Cell string
}
type stat struct{ n, night int }
func main() {
at := func(d, h int) time.Time {
return time.Date(2026, 10, d, h, 0, 0, 0, time.UTC)
}
recs := []Record{
{at(1, 23), "07700 900101", "cell-14"},
{at(2, 0), "07700 900101", "cell-14"},
{at(2, 1), "07700 900101", "cell-14"},
{at(2, 9), "07700 900202", "cell-3"},
{at(3, 12), "07700 900303", "cell-3"},
{at(4, 23), "07700 900101", "cell-14"},
}
stats := map[string]*stat{}
for _, r := range recs {
s := stats[r.To]
if s == nil {
s = &stat{}
stats[r.To] = s
}
s.n++
if h := r.When.Hour(); h >= 23 || h < 5 {
s.night++
}
}
nums := make([]string, 0, len(stats))
for num := range stats {
nums = append(nums, num)
}
sort.Slice(nums, func(i, j int) bool { return stats[nums[i]].n > stats[nums[j]].n })
for _, num := range nums {
fmt.Printf("%s: %d events, %d at night\n", num, stats[num].n, stats[num].night)
}
}
The output ranks 07700 900101 first, with every event but none of the others after 23:00. You have not read a single word, and you already have a guess about a close relationship.
Where the cell column gives you a home address
The cell ID is the quiet one. A handset that keeps attaching to the same mast overnight is probably near where its owner sleeps. One that attaches to a different mast from 09:00 to 17:00 on weekdays is probably near work.
It is far coarser than GPS, from a few hundred metres in a city to several kilometres in the countryside. But repeated coarse locations combine into a very good picture. Add a second person's log and you can see when two handsets share a mast, which is a meeting.
Quick detour: why SMS is the oddest case
Hang on, a lot of people reading this will say "I never text". True: iMessage, WhatsApp and Signal traffic travels as ordinary data, so your carrier's record shows a data session to some server, not a message to a person. The operator of the app sees more than your carrier does.
Ordinary SMS and voice calls are the ones that leave a per-contact trail at the operator. Whether they appear on your own bill is a separate question, and it depends on the tariff; many plans do not itemise texts at all. Operators can still hold records for their own purposes.
What UK law calls it
The Investigatory Powers Act 2016 splits this out from content. Section 261 defines "communications data", and in broad terms it covers who, when, where and how, while the content of a communication is treated separately. The full definitions are on legislation.gov.uk.
The practical consequence, as I read it, is that the legal thresholds and authorisation routes for obtaining communications data differ from those for content. That is the law as written; whether the distinction still makes sense when metadata reveals so much is a matter of opinion, and mine is that it is shakier than it was in the days of landline bills.
Separately, UK GDPR applies to a provider's own use of these records. A subscriber's call history is personal data, and a subscriber access request is one way to see what a provider holds. What you get back depends on the provider and the circumstances.
What you can do about it
- Prefer end-to-end encrypted messengers for conversations; they move the message from the carrier's record into an app's, which at least reduces what the carrier holds.
- Do not assume that encryption hides the pattern. The app server still sees who connected and when, depending on how the app is built.
- If you run a service that stores logs, treat contact graphs and timestamps as sensitive in their own right, not only the payload.
The fake log above had six rows. A real year of one person's records has thousands, and the conclusions get a great deal firmer with every row.