# Implementation: object storage, warehouses, search and streaming
Verified against vendor documentation as at 05.08.2026. Databases and caches are in `impl-databases.md`.
## Amazon S3
The versioning trap is the most common object-storage retention defect. On a versioning-enabled bucket, deleting without a version id writes a delete marker; the object version becomes noncurrent and survives.
Complete rule set for a versioned bucket:
```xml
expire-current-versions
uploads/
Enabled
365
30
1
7
clean-expired-delete-markers
uploads/
Enabled
true
```
- Removing a noncurrent version requires **both** `NoncurrentDays` and, where set, `NewerNoncurrentVersions` to be exceeded.
- `ExpiredObjectDeleteMarker` cannot share a rule with `Days` — hence the second rule.
- `ExpiredObjectDeleteMarker` and `AbortIncompleteMultipartUpload` rules cannot use a tag-based filter.
- Lifecycle actions run asynchronously; objects can persist past the nominal expiry date. Monitor actual object age rather than trusting the rule's `Days`.
- Replication is separate: a replicated object has its own lifecycle, and delete-marker replication is a distinct setting.
- **Object Lock in compliance mode blocks deletion until the retention date, for every principal including the root user.** Such a bucket is one where an Art 17 request cannot be executed. Record it as an erasure exception in the schedule.
## Cloudflare R2
```sh
npx wrangler r2 bucket lifecycle add [OPTIONS]
```
The S3-compatible API accepts `putBucketLifecycleConfiguration` with `Expiration`, `Transitions` and `AbortIncompleteMultipartUpload`. Deletion is not immediate: the documentation states objects are *"typically removed from a bucket within 24 hours of the `x-amz-expiration` value"*.
`[[UNVERIFIED: whether R2 supports object versioning as at 2026-08. The object-lifecycle documentation does not mention it; confirm before assuming the S3 delete-marker trap does not apply]]`
## Google BigQuery
```sql
-- expire partitions after 90 days
ALTER TABLE mydataset.events
SET OPTIONS (partition_expiration_days = 90);
-- remove the expiry
ALTER TABLE mydataset.events
SET OPTIONS (partition_expiration_days = NULL);
```
```sh
# same via CLI, in seconds
bq update --time_partitioning_expiration 7776000 --time_partitioning_type DAY mydataset.events
# drop one partition outright
bq rm --table 'mydataset.events$20260301'
```
Row-level deletion:
```sql
DELETE FROM mydataset.events
WHERE _PARTITIONDATE IN ('2026-03-06', '2026-03-07');
```
The recovery envelope: 7-day time travel, configurable only between 2 and 7 days via `max_time_travel_hours`, plus a 7-day fail-safe that is not configurable and not queryable — *"You can't query or directly recover data in fail-safe storage. To recover data from fail-safe storage, contact Cloud Customer Care."* Deleted data can persist **up to 14 days**. A table expiration takes precedence over a partition expiration.
## Elasticsearch and OpenSearch
```json
PUT _ilm/policy/logs-90d
{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_primary_shard_size": "50gb", "max_age": "1d" } } },
"warm": { "min_age": "7d", "actions": { "forcemerge": { "max_num_segments": 1 } } },
"delete": { "min_age": "90d", "actions": { "delete": { "delete_searchable_snapshot": true } } }
}
}
}
```
- `min_age` is measured from rollover, not from index creation, once rollover is in play. Getting this wrong is the usual reason an index outlives its policy.
- `delete_searchable_snapshot` defaults to `true`. If an index is deleted manually before ILM reaches the delete phase, ILM will not clean up the underlying searchable snapshot; it must be removed with the delete-snapshots API.
- Attach with `index.lifecycle.name` in the index template, or configure the data stream, so new backing indices inherit the policy automatically.
- **`delete_by_query` marks documents deleted; the data remains in the segment until merge.** For a subject erasure with a deadline, follow it with a force merge that expunges deletes, and record that step in the runbook.
- OpenSearch uses ISM (`_plugins/_ism/policies`) with equivalent states and a `delete` action; the marked-then-merged semantics are the same.
## Apache Kafka
```sh
kafka-configs.sh --bootstrap-server localhost:9092 --alter \
--entity-type topics --entity-name events \
--add-config retention.ms=2592000000,segment.ms=86400000
```
`cleanup.policy` semantics, quoted from the Kafka topic-configuration documentation: the `delete` policy (the default) *"will discard old segments when their retention time or size limit has been reached"*; the `compact` policy *"will enable log compaction, which retains the latest value for each key"*; both may be given as `delete,compact`; **an empty list means infinite retention — no cleanup policies will be applied and log segments will be retained indefinitely** (behaviour noted in the Kafka 4.2.0 upgrade notes).
The compaction trap: on `cleanup.policy=compact`, `retention.ms` does not remove a key's latest value. The only removal path is a **tombstone** — a record with the same key and a `null` payload — after which the delete marker is itself cleaned out following a configurable retention period:
```
produce(topic, key = subject_key, value = null) # tombstone
# then wait for: min.compaction.lag.ms, the cleaner pass, and delete.retention.ms
```
Relevant knobs: `delete.retention.ms` (how long tombstones survive so consumers can observe them), `min.cleanable.dirty.ratio`, `min.compaction.lag.ms` / `max.compaction.lag.ms`, `segment.ms`. The active segment is never compacted, so a low-traffic topic can hold an uncompacted key for a long time.
Also check `log.local.retention.ms` / `log.local.retention.bytes` where tiered storage is enabled, and every consumer that has materialised the topic into its own state store — the tombstone must propagate there too.
## Checkpoints
- [ ] Every versioned bucket has `NoncurrentVersionExpiration` **and** an `ExpiredObjectDeleteMarker` rule
- [ ] `AbortIncompleteMultipartUpload` configured wherever multipart uploads occur
- [ ] Replication targets have their own lifecycle rules; delete-marker replication decided deliberately
- [ ] Object Lock compliance-mode buckets identified and recorded as erasure exceptions
- [ ] BigQuery `max_time_travel_hours` set deliberately; the 7-day fail-safe included in the published period
- [ ] ILM `min_age` verified against rollover semantics, not index creation
- [ ] Force merge follows any `delete_by_query` used to satisfy an erasure request
- [ ] Every compacted Kafka topic has a documented tombstone procedure, including downstream state stores
- [ ] No Kafka topic has an empty `cleanup.policy` unintentionally
- [ ] Actual object and index ages monitored, not just the configured rule