Session replay operations
Recordings are rrweb events, scrubbed at wt-edge, gzip-compressed and stored in S3 at replay/{project}/{session}/{seq:08}. Metadata lives in ClickHouse (replay_sessions_v1, replay_chunks_v1). Bytes are only served through the API after a project check.
Retention
Each session carries the project's
replay_retention_daysas of its first chunk. Changing the setting applies to new sessions.POST /v1/projects/{p}/replays/retention/apply(ManagePrivacy, audited) rewrites existing sessions to the current value.ClickHouse TTL removes metadata. The worker's daily job (
replay_retention) removes the S3 objects of sessions pastlast_seen + retention_days, deletes objects with no metadata older than one day, and marks metadata whose objects are gone as deleted. Counts appear inwt_replay_orphans_total{kind="object|metadata"}.A legal hold (
POST /v1/projects/{p}/replays/{session}/legal-hold,DELETEto release) exempts one session from retention and from bulk deletion until released. A hold raises the session's ClickHouse retention to the maximum (65535 days); release restores the project's current value.
Deletion
DELETE /v1/projects/{p}/replays/{session}: removes the S3 prefix, then setsdeleted = 1and drops chunk metadata.DELETE /v1/projects/{p}/replays?before=<RFC 3339>deletes up to 500 sessions per call (call again whilemoreis true). Both needManagePrivacyand are audited.Deleting a project queues a purge of
replay/{project}/(purge_project_replays, also the entry point for the Phase 6 purge job).Every deletion writes a
replay_deletions_v1row first. If the API or worker dies midway the worker finishes it on its next pass, so a started deletion always completes.
Deletion guarantee
Once a delete call returns 204 the recording is not readable through Watchtower. In a bucket with versioning enabled the objects remain as noncurrent versions until the bucket's lifecycle rule expires them, and backups of the bucket or of ClickHouse keep copies until the backup's own retention ends. Set the noncurrent-version expiry and backup retention at or below the deletion promise you make to your users.
Bucket lifecycle example
Defense in depth for the worker job (set the days to the longest replay_retention_days plus a week):
{
"Rules": [
{
"ID": "wt-replay-expire",
"Filter": { "Prefix": "replay/" },
"Status": "Enabled",
"Expiration": { "Days": 37 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 7 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
}
]
}Do not apply a shorter expiry than a held session needs: lifecycle rules know nothing of legal holds. Exclude held sessions by moving them under a separate prefix first, or leave the rule off and rely on the worker job.
Stats and sizing
GET /v1/projects/{p}/replays/stats (ReadReplay) returns sessions, bytes, bytes per day (90 days), the top 10 routes by bytes and projected_steady_state_bytes (avg_daily_bytes * retention_days, averaged over the last seven recorded days).
Sizing rule of thumb: measure bytes / sessions for your traffic. Scrubbed, gzipped sessions are typically a few hundred KiB at the median, with a long tail from long sessions and heavy DOMs. Budget sessions per day * median bytes * retention days, plus 30% for the tail.
Export
GET /v1/projects/{p}/replays/export?from=&to=&format=rrweb-ndjson streams a zip with one {session}.ndjson per session (one rrweb event per line, readable by rrweb tooling). Needs ReadReplay (Engineer and above); audited; at most 500 sessions per call (narrow the range otherwise). The data is the scrubbed copy; unmasked content never reaches storage.
Not yet built: copying new chunks to a project export destination (S3 or Kafka) as they arrive. Until it lands, use the export API on a schedule.
Self-hosted storage notes
Any S3-compatible store works (
WT_ARTIFACT_STORE=s3://bucket,AWS_*variables). Afile:///dirstore is for single-node trials only.The API and worker need
s3:GetObject,PutObject,DeleteObjectandListBucketon thereplay/prefix. Chunk writes are idempotent by(session, seq).Size the bucket from the stats projection; alert on
wt_replay_orphans_totalrising, which means writes and metadata are diverging.