Analytics, logs and costs

What is recorded about every request, where it goes, what it costs, and every setting that changes it.

#Analytics, logs and costs

Every request your instance serves is recorded. This page explains what is recorded, where it goes, what it costs, and every setting that changes it.

If you only read one section, read the three places data lives — most questions about "why can I see this here but not there" have their answer in that table.

#Why there is more than one system

It is tempting to expect a single log. There are three stores instead, and the reason is that one store cannot be cheap, complete and deletable at once:

  • Recording every request cheaply means a store that samples under extreme load and cannot be edited.
  • Proving a specific thing happened means a store that never samples.
  • Honouring "delete my data" means a store you can delete from.

So EmuView uses each where it is strongest, and this page is mostly about which is which.

#The three places data lives

Analytics Durable log Archive
Holds Every request Sign-ins, refusals, exports, backups, downloads, changes Copies of both
Where Cloudflare Analytics Engine Your database Your R2 storage
Kept for ~90 days (fixed) Until you set a retention As long as you choose
Complete? Sampled under very high load Always complete As complete as its source
Deletable? No — entries age out and cannot be removed Yes Yes
Costs Per request written, and per query run Database rows Storage
Read at System → Logs → Access, System → Usage System → Logs → Access, System → Logs → Audit On demand

The analytics stream cannot be turned off. It is what the usage ledger, the cost reports, the performance dashboard and the access log are all built on. There is no setting to disable it, and that is deliberate: turning it off would break billing and capacity reporting to save a sum measured in cents.

#What is recorded about a request

Every request contributes: the workspace, the HTTP method, the endpoint pattern (/api/v1/collections/:name/records, never the specific record id), the status, the outcome, how long it took, how much database and storage work it did, any AI tokens it used, the country, how the caller authenticated, and an account reference — see privacy.

A smaller set of events also gets a durable database row, because losing them would matter:

Event Why it is kept durably
Sign-in, sign-out, password change, two-factor The security baseline
Anything refused (401, 403, rate-limited) The most valuable signal there is, and self-limiting
Exports, backups, downloads Data leaving the instance
Operator reads across workspaces The most privileged read there is
Server errors
Creating, changing or deleting data What the audit trail has always covered

Ordinary successful reads are not duplicated into the database. They are in analytics with full detail, and copying every one of them into your database is the expense this design exists to avoid.

Refusals were invisible before this. A refused request changes nothing, so nothing recorded it — which meant "what was this account refused, and when?" had no answer anywhere. It does now.


#Reading the access log

System → Logs → Access. Two views:

Events — what happened, newest first. Filter by period, outcome, actor or endpoint. Clicking an actor or an endpoint filters to it, and the filters live in the address bar, so a filtered view is a link you can send to someone.

Reports — three summaries: who is being refused and where, the busiest accounts, and the slowest endpoints. Slowest is ranked by average time per call, not total, so it does not simply repeat the busiest list.

#When a window was sampled

Under very high traffic Cloudflare samples the analytics stream: each stored entry then stands for several real requests. When that happens the screen says so, and it means counts are estimates and the event list is a subset. It is not an error, and it does not affect the durable log — which is precisely why sign-ins and refusals are kept there.

#The three empty states

They mean different things and the screen distinguishes them:

  • "You do not have access" — you need the audit-log read permission.
  • "No events" — the log is working; nothing matched.
  • "Access logging is not configured" — analytics credentials are missing, so there is nothing to read. This is a broken pipe, not a quiet instance.

#Privacy: what identifies a person

This is the part worth understanding properly, because it is what lets you answer a data-subject request honestly.

#Account references, not names

The analytics stream never contains a user id or an email address. It contains an account reference — an opaque token derived from the account plus a secret held only in your database.

The reason is the constraint at the top of this page: nothing can be deleted from the analytics stream. Had it contained real identities, no erasure request could ever be honoured for it. Instead:

  • The screen shows real names, resolved from your database when you look.
  • Erasing an account destroys the secret, and every historical entry becomes permanently unlinkable — not merely un-looked-up, but impossible to recompute, because the secret was an ingredient rather than a lookup key.
  • Erased accounts then display as "deleted user". Seeing that is the proof the erasure worked.

Each workspace gets a different secret, so the same person in two workspaces has two unrelated references and cannot be correlated across them.

What erasure does not do: it does not delete the account, its records, or the durable audit rows naming it. Those are separate, deletable, and governed by your own audit policy — an audit trail is frequently required to outlive the person it describes, so removing it is a decision for you rather than a side effect.

#IP addresses

An IP identifies a person and — unlike an account reference — cannot be anonymised afterwards. So the only controls are keeping less, or none:

Mode Stores Use when
Truncated (default) 203.0.113.0 — the network You want "same network as before?" and "which country?" without singling anyone out
Full 203.0.113.47 You are investigating an incident and need the exact connection
None nothing Your policy forbids it

Addresses are only ever taken from Cloudflare's own connection record, never from a header a client can set — otherwise a caller could choose what appears in your audit trail.

The user agent is off by default. It helps spot an unfamiliar client on an account, and it is also a fingerprinting surface, so you opt in.


#Retention and archiving

#Keeping the database bounded

Durable log rows stay forever unless you set a retention. The default is "keep everything" — an upgrade should never quietly start deleting an audit trail.

When you set one, the hourly maintenance job deletes durable access entries older than that many days. It touches only the entries this system writes — sign-ins, refusals, exports, downloads, errors. Records of changes to your data are never removed by this setting; they are yours to govern.

Retention is per workspace: the setting lives on each workspace's own Settings → Access screen, and the maintenance job reads and applies each workspace's own value. On an instance with more than one workspace, one workspace setting a retention has no effect on any other.

#Archiving beyond 90 days

The analytics stream ages out at ~90 days. Turn on Archive to storage and, before that happens, each day is copied into your R2 storage as:

analytics/access-log/{workspace}/{YYYY-MM}/{DD}.db        one SQLite file per day
analytics/access-log/{workspace}/{YYYY-MM}/summary.json   a small monthly summary

Each day file holds both the analytics events and that day's durable rows. Each workspace has its own folder, so exporting or deleting one workspace's history is a single operation and no query can span workspaces — and each workspace is archived on its own, from its own setting, into its own folder.

How much the hourly job does. One day per workspace per hour, oldest gap first, for the three workspaces furthest behind — the expensive part of archiving is building the SQLite file, and an hourly maintenance job must not become a long-running one. That clears a backlog quickly for the workspaces that have one while leaving the rest untouched, and up to about seventy workspaces archiving continuously stays comfortably ahead of the 90-day window. If more workspaces are behind than the hour can reach, the Scheduled maintenance panel says so rather than quietly falling behind.

Why day files rather than one per month: a busy month is far larger than the memory a Worker is allowed, and a day is comfortably within it.

Why a summary as well: it answers "show me three years of trends" without opening a single day file. A year of summaries is a few hundred kilobytes.

Why archiving runs three days behind: analytics ingestion is not instantaneous, and a day archived at midnight would be permanently missing its last hours. Waiting costs nothing against a 90-day window.

Why the archive is written before anything is deleted: with archiving on, retention is clamped to the last day actually written to storage — that workspace's storage, not any other's. If a workspace's archive falls behind, its deletion waits and the health panel names it. A retention job that outran its archive would destroy history while reporting success, and one that read a different workspace's archive position would do it across workspaces.

Archives are still tidied up after you switch archiving off. Turning Archive to storage off stops new day files being written; it does not abandon the ones already there. Keep archives for (months) keeps applying, so old files still age out instead of accumulating in storage you pay for.

File format. Archives are SQLite, readable by any SQLite tool. Parquet was considered — it is columnar and opens in DuckDB, Spark and pandas — but there is no Parquet writer that runs in this environment today, while SQLite is already proven here. If you need columnar files later, converting these is a batch job, not a migration.


#What it costs

#The analytics stream

Cloudflare bills it two ways, and both are generous:

  • Writing: per request recorded. Workers Paid includes 10 million data points a month. A busy small instance uses a fraction of that.
  • Reading: per query, flat — the same whether a query scans one day or ninety. One million read queries are included monthly.

That second point shapes the product: the screens deliberately run one query per view rather than one per row. It also means you should not hesitate to widen a time range — a 90-day query costs exactly what a 1-hour query costs.

#The durable log

Ordinary database rows. Kept small on purpose: only the events listed above land there, which is a tiny fraction of traffic. This is why an ordinary browsing session writes nothing to your database.

#The archive

R2 storage, billed by what you keep. Roughly a few megabytes per busy workspace per day, and less for a quiet one. Set an archive retention if you want a ceiling; monthly summaries are always kept because they are negligible.


#Settings reference

Settings → Access Log.

Setting Default What it does
IP address recording truncated Full address, network only, or nothing
Record user agent off Store the browser/client string
Durable retention (days) 0 (forever) Delete durable entries older than this
Archive to storage off Copy each day into R2 before it ages out
Keep archives for (months) 0 (forever) Delete archived day files older than this

Related, under Settings → Analytics: data retention, cost receipts, cache strategy and TTL, Cloudflare credentials, and alert destinations.

There is deliberately no setting to sample or disable the analytics stream. One existed until it was found to do nothing at all, and it was removed rather than repaired: writing everything is what makes the usage ledger and the access log trustworthy, and it costs the same per entry regardless.


#With more than one workspace

Everything here is per workspace.

  • Separate by default. Each workspace's events carry its own id, and every query is scoped to the workspace the caller belongs to. There is no parameter that points one workspace's screen at another's data.
  • Different account references. Per-workspace secrets mean the same person appears as two unrelated references, so activity cannot be correlated across workspaces even by someone holding both logs.
  • Settings are per workspace. A workspace can record more than the operator default — but never less than the floor the operator sets.
  • Archives are per workspace, in their own folder, so export and deletion are prefix operations.
  • Operators see across workspaces through the separate operator views under System, which require the operator permission — deliberately not the ordinary audit permission a workspace administrator holds. Those cross-workspace reads are themselves recorded, in the workspace being read.

See What is multi-tenancy? for how workspaces work generally.


#Common questions

Someone is being refused and I do not know why. System → Logs → Access → Reports → Most refused. Click the account to see every refusal with its endpoint and time.

I need to prove an export happened. Exports are durable, so they survive sampling and any retention you have not explicitly set. Filter the events view by outcome, or search the audit log under System → Logs → Audit.

Somebody asked to be deleted. Erasing their access-log identity severs every historical entry from them permanently. Their account, records and audit rows are separate steps under your own policy.

My log looks empty. Check which of the three empty states is showing. "Not configured" means credentials are missing, not that nothing happened.

Why can I see a request in analytics but not in the audit log? Because it was an ordinary read. Only the events in the table above are kept durably.