Replication¶
Arca replicates objects asynchronously from a source bucket to a destination bucket on any S3-compatible endpoint. The destination can be another Arca instance, AWS S3, MinIO, or any server that speaks the S3 API and signs with SigV4.
Replication is one-way per rule, but two-way mirrors are supported safely by configuring symmetric rules on both sides — the loop-prevention contract (see below) guarantees no ping-pong.
What gets replicated¶
| Event | Replicated? |
|---|---|
PutObject |
Yes |
CompleteMultipartUpload |
Yes |
CopyObject (destination side) |
Yes |
PutObjectTagging / DeleteObjectTagging |
Yes |
| Delete marker on versioned bucket | Yes (opt-in per rule, default on) |
| Hard delete of a specific version | No (AWS semantics) |
| Object Lock / retention changes | Not yet (tracked for a later phase) |
| ACLs | Not yet |
The destination bucket must already exist — Arca does not auto-create it.
Prerequisites¶
- Versioning must be Enabled on the source bucket. Replication operates on object versions;
PutBucketReplicationreturnsInvalidRequestotherwise. Enable versioning first (console → Bucket Settings → Versioning → toggle). - A destination credential stored in the source instance's server settings, under
replication.credentials.<name>. The console's rule editor can create one inline; the admin API accepts direct POSTs (see below).
Quick Start — console¶
- Open Buckets → bucket name → Settings on the source instance. Scroll to the Replication card.
- If the card shows an amber "Versioning required" banner, click Enable versioning (the link scrolls to the Versioning card), toggle it on and reload.
- Click + Add Rule.
- Fill in:
- Rule ID: any stable string (auto-generated by default).
- Prefix (optional): replicate only objects whose key starts with this string.
- Tag filter (optional): click + Tag to require one or more
key=valuetags. An object must carry all listed tags to match. Combine with prefix to narrow further — both conditions must hold. - Destination Bucket, Endpoint URL (e.g.
https://replica.example.com), Region. - Destination credentials: pick an existing one, or choose + New, enter a credential name plus the access-key / secret, and save.
- Replicate delete markers: default on.
- Click Add rule.
The replication worker picks up new objects on its next tick (default 15 s). Each object's x-amz-replication-status header moves PENDING → COMPLETED on the source and arrives as REPLICA on the destination.
Quick Start — CLI / direct API¶
1. Store the destination credential¶
curl -X POST "http://localhost:9000/admin/replication/credentials/replica-creds" \
--aws-sigv4 "aws:amz:us-east-1:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"access_key_id":"AKIA...","secret_access_key":"..."}'
2. PUT the replication configuration¶
aws s3api put-bucket-versioning --bucket source-bucket \
--versioning-configuration Status=Enabled \
--endpoint-url http://localhost:9000
cat >/tmp/repl.xml <<'EOF'
<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Role></Role>
<Rule>
<ID>to-replica</ID>
<Status>Enabled</Status>
<Priority>1</Priority>
<Filter><Prefix></Prefix></Filter>
<Destination>
<Bucket>replica-bucket</Bucket>
<Endpoint>https://replica.example.com</Endpoint>
<Region>us-east-1</Region>
<CredentialRef>replica-creds</CredentialRef>
</Destination>
<DeleteMarkerReplication><Status>Enabled</Status></DeleteMarkerReplication>
</Rule>
</ReplicationConfiguration>
EOF
curl -X PUT "http://localhost:9000/source-bucket?replication" \
--aws-sigv4 "aws:amz:us-east-1:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "Content-Type: application/xml" \
--data-binary @/tmp/repl.xml
Filter shapes¶
The <Filter> block accepts three forms (matching the AWS ReplicationConfiguration surface):
- Prefix only — replicate every object under the prefix:
- Single tag, no prefix — replicate every object that carries this exact tag:
- Prefix plus one or more tags — replicate only objects matching all listed conditions, via
<And>:
Tags are evaluated against the object's tag state at the moment of the replicated operation. Tagging an existing object via PutObjectTagging emits a separate tag-replication event; it does not retroactively replicate the object itself (AWS CRR semantics — use S3 Batch Replication for backfill).
3. Upload an object and verify¶
aws s3 cp ./hello.txt s3://source-bucket/hello.txt --endpoint-url http://localhost:9000
# Replication status on the source
aws s3api head-object --bucket source-bucket --key hello.txt \
--endpoint-url http://localhost:9000 \
| jq -r '.ReplicationStatus'
# COMPLETED (after the worker's next tick)
# On the destination, the replicated object carries status REPLICA
aws s3api head-object --bucket replica-bucket --key hello.txt \
--endpoint-url https://replica.example.com \
| jq -r '.ReplicationStatus'
# REPLICA
The loop-prevention contract¶
Replication extensions Arca adds on top of the standard S3 replication surface:
- Every outbound request from the replication worker carries a custom header
x-amz-arca-replication-source: <source_endpoint_id>. The value comes from[replication].source_endpoint_idin the TOML config (defaultarca; set a unique stable string per deployment in production). - When an Arca instance receives a
PutObject/DeleteObject/PutObjectTaggingwhose request carries that header, it: - Stamps the object's
replication_statusasREPLICAinstead of computing a PENDING emit. - Skips the journal emit entirely — no journal row is inserted.
- When the emit logic checks a rule's filter, it short-circuits on REPLICA objects, so they are never re-emitted by a rule configured on the receiving side.
This is what makes two-way mirrors safe. Configure the same-named bucket on both Arcas with symmetric rules: a client-originated PutObject on A fires exactly once on A's journal, shows up on B as a REPLICA (no emit), and stays. No ping-pong. The test_mirror_does_not_loop integration test asserts this invariant.
Caveat: destinations that are not Arca (AWS S3, MinIO, …) ignore the custom header, so two-way mirrors against non-Arca destinations are not supported — use one-way only.
Two-way mirror setup (Arca ↔ Arca)¶
- On instance A, create the destination credential (pointing at B) and a rule
source-bucket → source-bucketwithEndpoint=B. - On instance B, create a destination credential (pointing at A) and a rule
source-bucket → source-bucketwithEndpoint=A. - Enable versioning on
source-bucketon both sides.
Writes on either side now reach the other; convergence is eventual.
Replication journal¶
Every pending / in-flight / failed delivery lives in the replication_journal table. The admin console has a dedicated Replication view at #/replication — it's the admin's single pane of glass:
- Created / Bucket / Key / Rule / Event type / Destination / Status / Tries / Actions columns with inline column-header filters.
- Color-coded status chips, pulsing on
in_flightentries. - Side panel with a dedicated "Flow" summary (source ➜ destination), the last error, and a per-entry Retry action.
Programmatic access:
# List journal entries
curl "http://localhost:9000/admin/replication/journal?status=failed" \
--aws-sigv4 "aws:amz:us-east-1:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
| jq
# Re-queue a specific entry
curl -X POST "http://localhost:9000/admin/replication/retry/<entry-id>" \
--aws-sigv4 "aws:amz:us-east-1:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
Retry policy¶
- The worker runs on a timer (
poll_interval_seconds, default 15 s) and claims up tobatch_sizepending rows per cycle (default 100). - On delivery failure, the entry's
attemptsis incremented andnext_retry_atis set tonow + retry_base_seconds * 2^(attempts-1)(capped at 1 hour). - After
max_retriesfailures (default 10) the entry is markedfailedand the source object'sreplication_statusis stampedFAILED. - Transient errors don't stamp
FAILED— only the terminal retry does. Clients polling HEAD won't see spurious FAILED blips during a brief network hiccup.
Conflict resolution¶
Before every PUT the worker does a HEAD on the destination:
- If the destination object's
Last-Modifiedis greater than or equal to the source's, the PUT is skipped (destination-wins). The journal entry is still markedcompletedand the source is stampedCOMPLETED. - If the HEAD fails (network issue, 4xx), the worker proceeds with the PUT — a destination that rejects the write will surface as a normal failure and retry.
This matches S3's eventual-consistency semantics: replicated writes never silently overwrite a newer destination version, and there is no cross-instance coordination protocol.
Journal retention¶
The existing retention-purge worker prunes completed journal rows older than journal_retention_days (default 30). A hard cap journal_max_age_days (default 90) prunes rows of any status, so even a destination that has been offline indefinitely won't blow up the journal. Both thresholds are exposed in the console Settings page next to the other retention knobs.
TOML configuration reference¶
[replication]
poll_interval_seconds = 15 # worker tick
batch_size = 100 # rows claimed per tick
max_retries = 10 # before FAILED
retry_base_seconds = 5 # exponential base; capped at 1h
request_timeout_seconds = 60
source_endpoint_id = "arca-east" # stable per-deployment; loop-prevention marker
journal_retention_days = 30 # completed rows purged after
journal_max_age_days = 90 # any row purged after
Or start a development replica alongside the primary:
bin/arca start -d --dev --replication # boots arca on :9000 and arca-replica on :9001
bin/test replication # runs the 4 boto3 integration tests
Related¶
- Access Control — destination credentials are just another SigV4 access-key/secret pair.
- Monitoring & Logging —
/admin/metricsexposes journal counters alongside the rest.