<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Tigris Object Storage Blog</title>
        <link>https://www.tigrisdata.com/blog/</link>
        <description>Tigris Object Storage Blog</description>
        <lastBuildDate>Tue, 01 Sep 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>Copyright © 2026 Tigris Data, Inc.</copyright>
        <item>
            <title><![CDATA[Conflict resolution is “fun”]]></title>
            <link>https://www.tigrisdata.com/blog/conflict-resolution-is-fun/</link>
            <guid>https://www.tigrisdata.com/blog/conflict-resolution-is-fun/</guid>
            <pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How Tigris resolves write conflicts across regions: timestamp-based
replication on FoundationDB, clock skew races, and the tradeoffs of each
bucket type.
]]></description>
            <content:encoded><![CDATA[<p>One of the hardest problems in distributed systems is conflict resolution, or
the same basic problem as merge conflicts in Git. Git merge conflicts happen
when your branch differs from upstream in a way that Git can’t easily work its
way around so it exposes both sides of the changes to humans and has the human
(or their agent) figure out which side is “correct”. Distributed systems don’t
really have this same flow as the scale of changes is often impossible for any
human or team of humans to keep up with.</p>
<p>As a result, we <em>really</em> want to have our own business logic define which side
of a conflict wins. FoundationDB doesn’t let us do that out of the box, so we
had to make our own layer. The neat part about being able to do this is that
this lets us control the replication behaviour of buckets based on user needs.
The three main ways it differs are with
<a href="https://www.tigrisdata.com/docs/buckets/locations/#single-region" target="_blank" rel="noopener noreferrer" class="">single-region buckets</a>,
<a href="https://www.tigrisdata.com/docs/buckets/locations/#multi-region" target="_blank" rel="noopener noreferrer" class="">multi/dual-region buckets</a>,
and <a href="https://www.tigrisdata.com/docs/buckets/locations/#global" target="_blank" rel="noopener noreferrer" class="">global buckets</a>.
I’m going to cover the replication differences in the order of complexity.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="single-region-buckets">Single-region buckets<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#single-region-buckets" class="hash-link" aria-label="Direct link to Single-region buckets" title="Direct link to Single-region buckets" translate="no">​</a></h2>
<p>One of the easiest conflict resolution methods we have is a
<a href="https://www.tigrisdata.com/docs/buckets/locations/#single-region" target="_blank" rel="noopener noreferrer" class="">single-region bucket</a>,
which prevents any need for it. In this mode all actions are reverse proxied to
the bucket’s region and any metadata changes are local to that region in
particular. This means that any other regions trust the changes made by the
bucket’s region and reject any changes made by any other regions. As a side
effect this also means that the data <em>does not</em> move between regions like other
buckets do. You’d think that would mean “if a region goes down, my bucket goes
down.” But we have implemented data proxying, which means that if that region’s
block storage is still available, other gateways can route requests to it and
you can still access your data come hell, high water, or Giant Meteors.</p>
<p>The conflict resolution flow is like this:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 01</span><span style="font-size:12px;color:#94a3b8">the single-region write path</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  ┌────────────┐      ┌────────────────────┐      ┌───────────────────────────────┐</div><div>  │ client     │ ───▶ │ nearest Tigris     │ ───▶ │ owning region                 │</div><div>  │ PutObject  │      │ gateway            │      │                               │</div><div>  └────────────┘      │                    │      │ 1. bytes ──▶ block store      │</div><div>                      │ which region owns  │      │ 2. metadata ──▶ FoundationDB  │</div><div>                      │ this bucket?       │      │    <span style="color:#64748b">(one gRPC commit)</span>          │</div><div>                      └────────────────────┘      │ 3. <span style="color:#4ade80">return success</span>             │</div><div>                                                  └───────────────┬───────────────┘</div><div>                                                                  │</div><div>                                                      <span style="color:#64748b">reads queue</span> │ <span style="color:#64748b">(async)</span></div><div>                                                                  ▼</div><div>                                                  ┌───────────────────────────────┐</div><div>                                                  │ replication worker            │</div><div>                                                  │ every other region gets       │</div><div>                                                  │ a read-only copy              │</div><div>                                                  └───────────────────────────────┘</div></pre></figure></div>
<p>The main tradeoff with a single-region bucket is that you trade <em>strict
consistency</em> (because there is only one possible writer) with <em>higher latency</em>
(unless all of your workload is geographically close to that Tigris region in
particular). This makes a Tigris bucket mostly behave like a region-locked S3
bucket with the exception of being able to query the files globally without
having to configure your client to access that particular region.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="global-buckets">Global buckets<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#global-buckets" class="hash-link" aria-label="Direct link to Global buckets" title="Direct link to Global buckets" translate="no">​</a></h2>
<p>That was nice and simple. In comparison, global buckets are not.</p>
<p>Global buckets let any region be authoritative for any aspect of the bucket or
any object in the bucket. Any changes get committed to the local FoundationDB
cluster and then lazily replicated out to the other regions. This also means
that we expect there to be some level of conflict. Imagine an object storage
bucket like a git repository. You end up having conflicts when multiple people
push to the same files. How do we decide who wins? You can’t just have a person
sit there and decide which version of an object is right all the time.</p>
<p>Remember that we run a separate FoundationDB cluster per region. FoundationDB
has a sequencer that gives version identity to everything in the cluster. Those
versions are also based on time so you think we’d just be able to use those and
compare them to know which version is the newest, right? Well, it turns out that
FoundationDB versionstamps aren’t comparable across clusters as they’re partly
based on the cluster’s creation date and all of our clusters were created at
different times as we scaled globally. So we have to use something else that
changes fairly constantly between clusters in a way that’s easy for us to
validate without too much extra effort. Ideally, it’s something we already keep
in sync for making sure everything else in the system works.</p>
<p>Like many other things in Tigris, we use time to determine who wins a conflict.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Using time for this sounds boring, but time synchronization is unironically one
of the <a href="https://youtu.be/zT71UvUxhjU" target="_blank" rel="noopener noreferrer" class="">most complicated things in computing</a>.
This is a field where phrases like “temporal smearing”, “false ticker” and
“clock skew” are thrown around freely and ends up being a mess in practice. It’s
a small miracle that any of this works in practice.</p></div></div>
<p>Here’s a paraphrased and reformatted version of our conflict resolution
function:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 02</span><span style="font-size:12px;color:#94a3b8">compare() decides which version of an object wins</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  compare(previous, new, force)</div><div><span style="color:#64748b">  │</span></div><div><span style="color:#64748b">  ├── </span>is prev or next unset, or a brand-new key in FDB?</div><div><span style="color:#64748b">  │      </span>yes: different objects <span style="color:#64748b">──────────▶</span>  <span style="color:#f87171">drop, log the conflict  ✗</span></div><div><span style="color:#64748b">  │</span></div><div><span style="color:#64748b">  ├── </span>is prev older than the new data?</div><div><span style="color:#64748b">  │      </span>yes: the new version is newer <span style="color:#64748b">───▶</span>  <span style="color:#4ade80">apply, row replaced     ✓</span></div><div><span style="color:#64748b">  │</span></div><div><span style="color:#64748b">  └── </span>tie, or new is older: is this a forced change?</div><div>         yes: tie forcibly broken <span style="color:#64748b">────────▶</span>  <span style="color:#4ade80">apply, row replaced     ✓</span></div><div>         no <span style="color:#64748b">──────────────────────────────▶</span>  <span style="color:#f87171">drop, log the conflict  ✗</span></div></pre></figure></div>
<p>Or: we prioritize the most recent entry when possible, accounting for deletions
and the like such that a newer delete wins over an older update. This combined
with eventual consistency means that there are potentially situations where one
region updates an object and another region deletes it at the same time. During
that small replication window you can get a situation where Singapore says an
object exists that Chicago says doesn’t exist, but in practice the replication
delay is small enough (single digit seconds or thereabout) that it doesn’t
matter.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>In theory we could have done this by breaking out exotic things like
<a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type" target="_blank" rel="noopener noreferrer" class="">conflict-free replicated data types</a>,
but that seems kinda overkill for our usecase. That would work great for the
source control merge conflict problem though!</p></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="time-keeps-ticking">Time keeps ticking<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#time-keeps-ticking" class="hash-link" aria-label="Direct link to Time keeps ticking" title="Direct link to Time keeps ticking" translate="no">​</a></h3>
<p>At this point clock skew is also a factor. We run NTP clients on all our
infrastructure and that usually keeps us within about 10 microseconds (10,000
nanoseconds) off of NTP time. Given that we measure timestamps as nanoseconds to
decide conflicts, clock skew-based ordering conflicts can genuinely be a factor.</p>
<p>Imagine that the two operations in Chicago were served by different servers that
just so happen to have their clocks off by a fraction of a fraction of a second.
A DELETE could be sequenced before a PUT and the object could be shown as
deleted in Chicago but present in Singapore because that PUT replicated out
<em>after</em> the delete was committed locally.</p>
<p>According to all the rules of the game, every region but Chicago sees that the
DELETE is slightly older than the current data, so it ignores that DELETE and
continues as if that PUT is the right state of the world.</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 04</span><span style="font-size:12px;color:#94a3b8">a clock-skew race resurrects a deleted object</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div> client          Chicago (bucket owner)         Singapore</div><div>    │                       │                       │</div><div>    │ PUT thing.txt         │                       │</div><div>    ├──────────────────────▶│                       │</div><div>    │                       │                       │   <span style="color:#64748b">stamped …000200</span></div><div>    │                       │                       │   <span style="color:#64748b">(the clock ran a hair fast)</span></div><div>    │                       │                       │</div><div>    │                       │ replicate @ …000200   │</div><div>    │                       ├──────────────────────▶│</div><div>    │                       │                       │   <span style="color:#64748b">Singapore stores the</span></div><div>    │                       │                       │   <span style="color:#64748b">live copy @ …000200</span></div><div>    │                       │                       │</div><div>    │ DELETE thing.txt      │                       │</div><div>    ├──────────────────────▶│                       │</div><div>    │                       │                       │   <span style="color:#64748b">the clock still reads …000100,</span></div><div>    │                       │                       │   <span style="color:#64748b">so the tombstone is stamped</span></div><div>    │                       │                       │   <span style="color:#64748b">BEHIND the row it deletes</span></div><div>    │ 204 No Content        │                       │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤                       │</div><div>    │                       │                       │</div><div>    │                       │ tombstone @ …000100   │</div><div>    │                       ├──────────────────────▶│</div><div>    │                       │                       │   <span style="color:#64748b">…000100 is not newer than</span></div><div>    │                       │                       │   <span style="color:#64748b">…000200: the tombstone is</span></div><div>    │                       │                       │   <span style="color:#64748b">dropped, the live copy survives</span></div><div>    │                       │                       │</div><div>    │ GET thing.txt         │                       │</div><div>    ├───────────────────────┼──────────────────────▶│</div><div>    │ <span style="color:#f87171">200 OK, the deleted object is back</span>            │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤</div><div>    │                       │                       │</div><div>    │ PUT If-None-Match: *  │                       │</div><div>    ├──────────────────────▶│                       │</div><div>    │ <span style="color:#f87171">412 Precondition Failed</span>                       │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤                       │</div></pre></figure></div>
<p>This is the kind of race condition you can only really see in distributed
systems at our scale or larger. Nothing that happens in this situation is a
failure in the logic or implementation, it’s just a desync because of
unfortunate timing.</p>
<p>I wish I could tell you an epic tale of time synchronization and other fun
things along the way of fixing this particular issue, but the fix was sadly
boring. We just refuse to process a DELETE when the DELETE is older than the
data in the database. If the DELETE doesn’t land after the database row’s date,
we re-read the clock and try again every 100 milliseconds up to 5 (five) times.
If even that can’t result in a timestamp that sorts correctly we give up loudly
to the client instead of quietly doing the wrong thing.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>-Wpedantic</div><div class="admonitionContent_BuS1"><p>Our conflict resolution is only as good as our clocks are. Time synchronization
like this is kind of a hard problem to solve and we’d like to avoid having to do
that if we can. If this becomes an issue in the future, we may have to build
sacrificial lamb servers with
<a href="https://store.timebeat.app/products/ocp-tap-timecard" target="_blank" rel="noopener noreferrer" class="">Cesium/Rubidium time cards</a>
and deploy those in our datacentres, kinda like the atomic clocks Google uses
for Spanner. That would certainly make for a cool project!</p><p>Come to think of it, the premise of Neal Stephenson’s Anathem (where a group of
timekeeping monks lock themselves in giant clocks as part of their timekeeping
practices) makes a lot more sense after working in distributed systems for as
long as I have. If timekeeping magic is all that we’ve known, it really is easy
to miss what really goes on.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="multi-region-buckets">Multi-region buckets<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#multi-region-buckets" class="hash-link" aria-label="Direct link to Multi-region buckets" title="Direct link to Multi-region buckets" translate="no">​</a></h2>
<p>Finally we have multi-region buckets. These are the most complicated as they
combine aspects of both single-region and global buckets. There wasn’t a
protocol off the shelf that would do this for us– FoundationDB uses Paxos*
internally, and I’ve read about MultiPaxos (and Matchmaker Paxos,
<a href="https://mwhittaker.github.io/publications/matchmaker_paxos.pdf" target="_blank" rel="noopener noreferrer" class="">Matchmaker MultiPaxos</a>,
it goes on…). It seems like everyone modifies the algorithm for their use case,
I wonder if there is a “true” Paxos outside of academia. Maybe it was
implemented by the one true Scotsman.</p>
<p>We replicate multi-region bucket metadata by having one region be the leader for
a group and having that leader actively push out changes as well as enqueueing
them like a global bucket. This means that the regions in the group get the data
faster than they would otherwise and commits to the leader mean that the data is
committed to all members of the group. It’s kinda like this:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 05</span><span style="font-size:12px;color:#94a3b8">a multi-region write with one follower down</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div> client          gateway         leader        follower 1      follower 2</div><div>    │               │               │               │               │</div><div>    │ PUT object    │               │               │               │</div><div>    ├──────────────▶│               │               │               │</div><div>    │               │ bytes + meta  │               │               │</div><div>    │               ├──────────────▶│               │               │</div><div>    │               │               │               │               │   <span style="color:#64748b">upload to the leader's block</span></div><div>    │               │               │               │               │   <span style="color:#64748b">store, commit the metadata:</span></div><div>    │               │               │               │               │   <span style="color:#64748b">ONE transaction writes the row</span></div><div>    │               │               │               │               │   <span style="color:#64748b">and enqueues replication</span></div><div>    │               │ <span style="color:#4ade80">committed</span>     │               │               │</div><div>    │               │◀─ ─ ─ ─ ─ ─ ─ ┤               │               │</div><div>    │               │               │               │               │</div><div>    │               │ apply this right now          │               │</div><div>    │               ├───────────────┼──────────────▶│               │</div><div>    │               │ apply this right now          │               │</div><div>    │               ├───────────────┼───────────────┼──────────────▶│</div><div>    │               │               │               │               │   <span style="color:#64748b">pushed to every follower</span></div><div>    │               │               │               │               │   <span style="color:#64748b">at once, in parallel</span></div><div>    │               │ <span style="color:#4ade80">ok</span>            │               │               │</div><div>    │               │◀─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤               │</div><div>    │               │ <span style="color:#f87171">error</span>         │               │               │</div><div>    │               │◀─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤</div><div>    │               │               │               │               │</div><div>    │ <span style="color:#4ade80">200 OK</span>        │               │               │               │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ┤               │               │               │</div><div>    │               │               │               │               │   <span style="color:#64748b">one follower is enough</span></div><div>    ·               ·               ·               ·               ·</div><div>    │               │               │               │               │   <span style="color:#64748b">seconds later, the queue delivers</span></div><div>    │               │               │               │               │   <span style="color:#64748b">the same change all over again</span></div><div>    │               │               │ queued copy   │               │</div><div>    │               │               ├──────────────▶│               │</div><div>    │               │               │               │               │   <span style="color:#64748b">same timestamp, not newer:</span></div><div>    │               │               │               │               │   <span style="color:#64748b">follower 1 drops it</span></div><div>    │               │               │ queued copy   │               │</div><div>    │               │               ├───────────────┼──────────────▶│</div><div>    │               │               │               │               │   <span style="color:#64748b">nothing here yet, so it applies:</span></div><div>    │               │               │               │               │   <span style="color:#64748b">follower 2 catches up</span></div></pre></figure></div>
<p>When a client writes to a multi-region bucket, Tigris forwards the write to the
leader and blocks there. If the leader can’t take it, the write errors out and
nothing changes. If the leader commits the change, the gateway actively pushes
that change out to the other regions in the group in parallel and waits for them
to commit before answering the client. Interestingly enough, it uses the same
codepath that the global replication workers use. Every region in the group is
going to get this change twice: once from the fan-out on the leader’s commit and
the other from the queue worker pushing it out a moment later.</p>
<p>This may also seem like a race condition, but remember that the replication rows
contain the before and after state, a-la
<a href="https://www.postgresql.org/docs/current/logical-replication.html" target="_blank" rel="noopener noreferrer" class="">Postgres logical replication</a>
or git commit syncing. If the current data in the database is newer or the same
as the data being pushed out, the change is ignored and the cluster moves on
with life:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 06</span><span style="font-size:12px;color:#94a3b8">the failed write that succeeds anyway</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div> client            gateway           leader          every follower</div><div>    │                 │                 │                   │</div><div>    │ PUT object      │                 │                   │</div><div>    ├────────────────▶│                 │                   │</div><div>    │                 │ commit the metadata                 │</div><div>    │                 ├────────────────▶│                   │</div><div>    │                 │                 │                   │   <span style="color:#64748b">the row is written and durable.</span></div><div>    │                 │                 │                   │   <span style="color:#64748b">there is no undo from here</span></div><div>    │                 │ <span style="color:#4ade80">committed</span>       │                   │</div><div>    │                 │◀─ ─ ─ ─ ─ ─ ─ ─ ┤                   │</div><div>    │                 │                 │                   │</div><div>    │                 │ apply this right now                │</div><div>    │                 ├─────────────────┼──────────────────▶│</div><div>    │                 │ <span style="color:#f87171">error</span>           │                   │</div><div>    │                 │◀─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤</div><div>    │                 │                 │                   │</div><div>    │ <span style="color:#f87171">5xx, your write failed</span>            │                   │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ┤                 │                   │</div><div>    │                 │                 │                   │   <span style="color:#64748b">no follower succeeded</span></div><div>    ·                 ·                 ·                   ·</div><div>    │                 │                 │                   │   <span style="color:#64748b">but the queued copy was already</span></div><div>    │                 │                 │                   │   <span style="color:#64748b">durable, committed in the same</span></div><div>    │                 │                 │                   │   <span style="color:#64748b">transaction as the row</span></div><div>    │                 │                 │ queued copy       │</div><div>    │                 │                 ├──────────────────▶│</div><div>    │                 │                 │                   │   <span style="color:#64748b">applies normally</span></div><div>    │                 │                 │                   │</div><div>    │                 │                 │                   │   <span style="color:#f87171">the write you were told failed is</span></div><div>    │                 │                 │                   │   <span style="color:#f87171">now readable in every region</span></div></pre></figure></div>
<p>At some level you can think about multi-region buckets as a latency optimization
for the regions in the multi-region group. The replication queue is what
actually guarantees the global convergence of metadata, but the active fan-out
over regions is what gets there first. This means that if you have a
multi-region bucket in the EU and all your workloads are in the EU, you get a
lot of the same availability advantages of global buckets without a lot of the
consistency risks of normal global buckets.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Given that this replication protocol isn’t a genuine two-phase commit (one phase
local, the second phase when the rest of the group has all committed), it means
that there is theoretically a case where a client can push a change to the
bucket, have that commit locally, fail to eagerly push out the changes at the
gateway level, and then return an error to the client after the data
successfully was written. The queue would then lazily replicate out the data
like nothing happened, meaning that the change would be pushed out even though
it technically failed.</p><p>HTTP doesn’t really have a good error code for this kind of partial failure and
the S3 API definitely does not either. If this becomes a problem in practice,
we’ll have to create an S3API extension for this. Stay tuned!</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="replication-brings-its-own-challenges">Replication brings its own challenges<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#replication-brings-its-own-challenges" class="hash-link" aria-label="Direct link to Replication brings its own challenges" title="Direct link to Replication brings its own challenges" translate="no">​</a></h2>
<p>Our current system has handled all customer load without too many issues. So
that any region can answer questions about the data, all metadata is replicated
to every region, even if it’s not requested anywhere else. This means that for
even single region buckets, there’s the same load on the replication queue as
there is for global buckets.</p>
<p>To work around this, we can compartmentalize the activities for these single
region buckets by sharding our FoundationDB clusters by bucket type, or even by
tenant, so the replication queue doesn’t risk lagging. We have a finite number
of replication workers and if too many objects change all at the same time it
can cause the replication delay to be minutes instead of seconds. The technical
term for this is “bad”.</p>
<p>Right now there’s one central FoundationDB cluster per region which stores
everything. In the future we plan to have one cluster that’s used to map
metadata about which buckets/organizations belong to which clusters, and from
there we will scale out depending on customer request pressure. If you’re a
large enough tenant, you may end up getting your own dedicated FoundationDB
cluster!</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 03</span><span style="font-size:12px;color:#94a3b8">sharding FoundationDB clusters into replicasets</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  S3 request</div><div>  │</div><div>  ▼</div><div>  ┌─────────┐ <span style="color:#64748b">── which replicaset holds this tenant? ──▶</span> ┌────────────────┐</div><div>  │ gateway │                                            │ shard mapping  │</div><div>  └─────────┘ <span style="color:#64748b">◀─ ─ ─ ─ ─ ─  </span><span style="color:#4ade80">"replicaset 2"</span><span style="color:#64748b">  ─ ─ ─ ─ ─ ─  </span>│ cluster        │</div><div>       <span style="color:#4ade80">║</span>                                                 └────────────────┘</div><div>       <span style="color:#4ade80">╚═══════════════════╗</span></div><div>                           <span style="color:#4ade80">▼</span></div><div>  ┌──────────────┐   <span style="color:#4ade80">╔══════════════╗</span>   ┌──────────────┐</div><div>  │ replicaset 1 │   <span style="color:#4ade80">║</span> replicaset 2 <span style="color:#4ade80">║</span>   │ replicaset 3 │</div><div>  │ 2-3 FDB      │   <span style="color:#4ade80">║</span> 2-3 FDB      <span style="color:#4ade80">║</span>   │ 2-3 FDB      │</div><div>  │ clusters +   │   <span style="color:#4ade80">║</span> clusters +   <span style="color:#4ade80">║</span>   │ clusters +   │</div><div>  │ worker pool  │   <span style="color:#4ade80">║</span> worker pool  <span style="color:#4ade80">║</span>   │ worker pool  │</div><div>  └──────┬───────┘   <span style="color:#4ade80">╚══════╤═══════╝</span>   └──────┬───────┘</div><div>         │                  │                  │</div><div>         └──────────────────┼──────────────────┘</div><div>                            ▼</div><div>                 <span style="color:#f87171">┌───────────────────────┐</span></div><div>                 <span style="color:#f87171">│ global pointer table  │</span></div><div>                 <span style="color:#f87171">└───────────────────────┘</span></div></pre></figure></div>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Pop quiz for people reading this post via social media: which Massively
Multiplayer Online Roleplaying Game is the origin of the term “sharding” and
what happened to cause that to need to be done? The first person to answer right
without searching wins the sense of pride and accomplishment that comes with
being right on the Internet first.</p></div></div>
<p>This does come with the obvious downside of having to manage multiple clusters,
but we think the tradeoff is worth it when it eliminates the problem of noisy
neighbors causing replication delay. It will mean that our caching layer has to
be a bit more complicated (at the very least we expect the mapping of
organizations/buckets to FoundationDB clusters to be fairly stable), but that’s
just a simple matter of programming at this point. It’s also gonna make
replication “fun”, but we’ll cross that bridge when we come to it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.tigrisdata.com/blog/conflict-resolution-is-fun/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>There’s three types of buckets but all of them use different shades of the same
global replication logic. Most of the levers we offer are all around
consistency, latency, and availability. Single region buckets remove the
question of where the data is stored at the cost of making that one region a
single point of failure. Global buckets embrace conflicts but can be a more
latent when changes are replicated out. Multi-region buckets double-replicate
your data across the cloud. All of these happen in regions that are far apart
enough that synchronous round trips are genuinely expensive, but everything is
just implementation details for the same basic object storage operations.</p>
<p>Computers really are something at this scale, aren’t they? The joys of our
industry know no end.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Need your own replicaset on Tigris?</span><p>If your workload is big enough, you could end up with a dedicated FoundationDB cluster all to yourself. Give us a shout and let's see if your data qualifies.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="mailto:help@tigrisdata.com" class="cta-link"><div>Get in touch<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>Object Storage</category>
            <category>foundationdb</category>
            <category>distributed systems</category>
            <category>replication</category>
        </item>
        <item>
            <title><![CDATA[Building a global object store on FoundationDB]]></title>
            <link>https://www.tigrisdata.com/blog/fdb-krea-talk/</link>
            <guid>https://www.tigrisdata.com/blog/fdb-krea-talk/</guid>
            <pubDate>Tue, 18 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How Tigris composes ACID metadata, global placement, caching, replication, and background work into a multi-region object store.
]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This post is an edited transcript of an in-person talk on June 16th. Parts have
been re-organized for your reading convenience.</p></div></div>
<img src="https://www.tigrisdata.com/blog/assets/images/title-8cfa5665e203cdf125d1d8dc47b6dc7e.webp" alt="Title slide: Building a global object storage on FoundationDB, by Himank Chaudhary, CTO and co-founder of Tigris Data. A ring of boxes labeled S3 API, global endpoint, replication, metadata, caching, and queue surrounds a FoundationDB box at the center.">
<p>Hello everyone! I'm Himank, the CTO and a co-founder of Tigris Data. Tigris is a
globally distributed object store.</p>
<p>Tonight other speakers talked about the scale of data involved with training
foundation models. Something has to hold all of that data. That's the part that
Tigris works on.</p>
<p>My talk will be focusing more on the metadata storage of Tigris, like how we are
using FoundationDB in our whole stack to power our metadata.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-tigris">What is Tigris?<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#what-is-tigris" class="hash-link" aria-label="Direct link to What is Tigris?" title="Direct link to What is Tigris?" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/what-is-tigris-d7a732f24df36e763fc856a72933beeb.webp" alt="What is Tigris? A global storage platform with an S3-compatible API. The S3 API makes Tigris a drop-in replacement for existing AWS SDKs, CLIs, and tools. All semantics are preserved. Tigris is global by default. A bucket can live in one or many regions. Reads are close to users; writes replicate in the background. FoundationDB at the core. Metadata, indices, and queues share one transactional substrate.">
<p>First, Tigris is a global storage platform that is fully S3 compatible. From a
user perspective you don't need to do any code changes. It will simple work with
your existing S3 stack. All you need to do is switch from your existing
S3-compatible provider to Tigris and it will just work.</p>
<p>Second, we're rethinking the object store from the ground up. Traditionally
object storage is known for cold caching. We're focusing on reframing object
storage as global infrastructure. How can we provide an object store as global
infrastructure? Users should not need to think about replication or caching. We
want users to get a bucket, upload data, and the data is available everywhere in
the world.</p>
<p>Finally, we want to make the data globally available and optimize the read
latency so that it doesn't matter where your compute is located. Tigris makes
your data follow your compute so you can pick from any compute or GPU provider
without worrying about your data being slow.</p>
<p>In order to make Tigris global by default, we're using FoundationDB. We store
all object metadata, users, and buckets in FoundationDB.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="read-and-write-in-any-regions">Read and write in any regions<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#read-and-write-in-any-regions" class="hash-link" aria-label="Direct link to Read and write in any regions" title="Direct link to Read and write in any regions" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/read-write-any-region-c5352533499c37bb8aabe498f019e098.webp" alt="Read and write in any region. The API gateway is present in each region. Metadata and blocks can be served locally, while replication keeps the global view coherent. Diagram of the global endpoint pointing to three datacentres, each with the API gateway, the metadata cluster, and block storage in US-East, EU-Central, and AP-Southeast">
<p>When we were designing our system, we were thinking a lot about how we can make
the components loosely coupled, but have an aggregated architecture where
storage is decoupled from other components. This would let us scale any of these
components independently.</p>
<p>For example, in each region we have our gateway, caches, metadata clusters,
block storage backends, and asynchronous queue workers. All of these services
can be scaled independently based on our needs, running on metal. The gateway
and workers are stateless, everything else is where the state lives.</p>
<p>So the obvious question at that point is what should we use for our metadata
storage? Metadata is very important. Tigris is mostly exciting ways to arrange
metadata with boring ways to store data.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-foundationdb">Why FoundationDB?<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#why-foundationdb" class="hash-link" aria-label="Direct link to Why FoundationDB?" title="Direct link to Why FoundationDB?" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/why-foundationdb-84864a85302e75d99723d30b5f618566.webp" alt="Why FoundationDB? Tigris builds metadata storage on top of an ordered key-value store with multi-key transactions. 1. ACID metadata. Mutations are one transaction, meaning there's no split-brain metadata. 2. Ordered keyspace. Versioned keys encode bucket, object, and index order. 3. Serializeable layout. Indices, metadata, and chunks mutate together. 4. Operational safety. High-availability, replication, and simulation-tested behaviour. 5. Battle-tested in production. Used by Apple and Snowflake as a building block for their cloud infrastructure.">
<p>There were a few options, either we built something on our own or used something
from outside. We picked FoundationDB for a few reasons:</p>
<ul>
<li class=""><strong>ACID metadata</strong>: In FoundationDB, mutations are one transaction. There is no
possibility of having split-brain metadata.</li>
<li class=""><strong>Ordered keyspace</strong>: FoundationDB keys have an inherent order, so versioned
keys encode bucket, object, and index order.</li>
<li class=""><strong>Serializeable layout</strong>: Indices, metadata, and chunks mutate together.</li>
<li class=""><strong>Operational safety</strong>: FoundationDB is high availability almost to a fault,
replicates cleanly without human intervention, and has simulation testing to
the level that Aphyr didn't even bother to evaluate it.</li>
<li class=""><strong>Battle-tested in production</strong>: FoundationDB is used by companies like Apple
and Snowflake as the foundation of their cloud infrastructure.</li>
</ul>
<p>At some level, it's best to think about FoundationDB as a distributed filesystem
that handles the hard parts for you: sharding, consensus, replication, and
transactions. You then get to build your own layer on top of it. FoundationDB
doesn't provide a schema, table layout, or indices. That's in the part you are
expected to provide.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="foundationdb-at-the-core">FoundationDB at the core<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#foundationdb-at-the-core" class="hash-link" aria-label="Direct link to FoundationDB at the core" title="Direct link to FoundationDB at the core" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/fdb-at-the-core-b4ba33efd6186cbb18589076c62c0cc3.webp" class="hero-image" alt="FDB at the core. The hard problem is not storing rows. It is making namespaces, indices, and background work change together. 1. Atomic across rows. Object rows, index rows, and queueing intent commit together in the same transaction. 2. Ordered ranges. Namespace scans, index lookups, and queue peeks are all range reads. 3. Operational substrate. Sharding, replication, and transactions are provided below Tigris.">
<p>Once we committed to FoundationDB we had to make a few changes to how we did
things. In FoundationDB it's impossible to read or write data without a
transaction. This sounds like a lot of overhead until you realize what it gives
us. A single object write usually involves reading the current state, updating
the new state, and then enqueueing the object for replication and indexing.</p>
<p>We also had to design our row layout, so we designed one that lets us support
efficient scanning for our users:</p>
<div data-fig="01" style="display:flex;flex-direction:column;gap:10px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;max-width:52rem;margin-left:auto;margin-right:auto"><div style="display:flex;align-items:baseline;gap:14px"><span style="font-size:11px;letter-spacing:0.16em;color:#f59e0b">FIG 01</span><span style="font-size:13px;color:#94a3b8">FDB row layout</span><span style="flex:1"></span><button style="font-family:inherit;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#64748b;background:transparent;border:1px solid #1e293b;border-radius:4px;padding:5px 10px;cursor:pointer">copy</button></div><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;line-height:1.3;white-space:pre;color:#cbd5e1;overflow-x:auto"><div><span>  </span><span style="color:#64748b">key</span><span>   </span><span style="color:#475569">·</span><span>   </span><span style="color:#64748b">lexicographic, one contiguous range per subspace</span><span>                 </span><span style="color:#64748b">value</span></div><div><span> </span></div><div><span>  </span><span style="color:#475569">┌──────────┬──────────┬────────────┬────────────────────────────────┐</span><span>      </span><span style="color:#475569">┌────────────────┐</span></div><div><span>  </span><span style="color:#475569">│</span><span style="color:#60a5fa"> tenant   </span><span style="color:#475569">│</span><span style="color:#60a5fa"> bucket   </span><span style="color:#475569">│</span><span style="color:#4ade80"> subspace   </span><span style="color:#475569">│</span><span style="color:#f59e0b"> object / index / key           </span><span style="color:#475569">│</span><span>      </span><span style="color:#475569">│</span><span> value          </span><span style="color:#475569">│</span></div><div><span>  </span><span style="color:#475569">├──────────┼──────────┼────────────┼────────────────────────────────┤</span><span>      </span><span style="color:#475569">├────────────────┤</span></div><div><span>  </span><span style="color:#475569">│</span><span style="color:#64748b"> t_9f3a   </span><span style="color:#475569">│</span><span style="color:#64748b"> photos   </span><span style="color:#475569">│</span><span style="color:#64748b"> obj        </span><span style="color:#475569">│</span><span style="color:#64748b"> 2026/08/img_001.jpg            </span><span style="color:#475569">│</span><span>  </span><span style="color:#60a5fa">──▶</span><span> </span><span style="color:#475569">│</span><span style="color:#64748b"> manifest ptr   </span><span style="color:#475569">│</span></div><div><span>  </span><span style="color:#475569">└──────────┴──────────┴────────────┴────────────────────────────────┘</span><span>      </span><span style="color:#475569">└────────────────┘</span></div><div><span> </span></div><div><span>  </span><span style="color:#475569">└───────────────────</span><span style="color:#64748b"> one FDB key, tuple-encoded </span><span style="color:#475569">────────────────────┘</span></div></pre><div style="font-size:12px;color:#475569;line-height:1.6;text-wrap:pretty;margin-bottom:1rem">The subspace byte decides what the remaining key means — object, index, or queue entry. Everything for one bucket is one contiguous range.</div></div>
<p>This lets us serve efficient queries for a single bucket. We don't need to worry
about sharding or multi-hit transactions. This comes by default with
FoundationDB.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-do-writes-do">What do writes do?<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#what-do-writes-do" class="hash-link" aria-label="Direct link to What do writes do?" title="Direct link to What do writes do?" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/write-path-c51de80079b12475437acba7d2553448.webp" alt="A PUT operation is a transaction plus a cascade of jobs. Strictly serializable transactions with ACID semantics across the key space. No performance penalty for cross-shard transactions. Indicies mutate in the same transaction causing strong consistency on index reads.">
<p>So what does a PUT look like? As I mentioned before, we use strictly ACID
transactions. A single write for us reaches the layer and we break the write
into the data block and the metadata. We write the block into block storage
first and then we start a FoundationDB transaction. In this transaction we write
object metadata and then update our indices along with that metadata. We also
have a bunch of other work that needs to be done after a write completes, so we
write to the queue in the same transaction.</p>
<p>What is that other work? Tigris is global, so we have to replicate the data
globally. We also have caches that need to be updated, so we have asynchronous
queue workers that can handle all this. But to run that async machinery we need
to have some kind of task mechanism that we do as part of our write.</p>
<p>Normally you end up having to juggle two transactions: one to your database and
another to your message queue. We implemented our message queue
<a href="https://www.foundationdb.org/files/QuiCK.pdf" target="_blank" rel="noopener noreferrer" class="">in FoundationDB</a> using the fact
that both FoundationDB and time are ordered. We don't have to run a distributed
transaction between two systems or add expensive recovery logic to ensure tasks
don't get lost. Either everything commits or nothing commits, which is one of
the best parts of FoundationDB.</p>
<p>Once this write commits and we know that any work items have been added to their
queues, we return to the user and the transaction completes successfully.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-we-resolve-gets">How do we resolve GETs?<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#how-do-we-resolve-gets" class="hash-link" aria-label="Direct link to How do we resolve GETs?" title="Direct link to How do we resolve GETs?" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/read-path-ffd8afad194e30f2bca7abc8f6712aa6.webp" alt="Local metadata chooses the GET path. A gateway in any region first resolves the object locally and then chooses the cheapest byte source. Metadata reads stay local. Cache hits avoid fetching data remotely. Cache misses stream once and populate caches.">
<p>How do we ensure that objects can be served from any region when the data may be
stored in any other region?</p>
<p>One way to think about Tigris is that it's a multi-tier cache that has endpoints
all over the world. When users request objects, a combination of anycast routing
and geo-DNS make sure that requests go to the closest datacenter. All our
metadata is eagerly replicated between FoundationDB clusters in each region, and
that metadata includes where the object actually lives.</p>
<div data-fig="02" style="display:flex;flex-direction:column;gap:10px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;max-width:52rem;margin-left:auto;margin-right:auto"><div style="display:flex;align-items:baseline;gap:14px"><span style="font-size:11px;letter-spacing:0.16em;color:#f59e0b">FIG 02</span><span style="font-size:13px;color:#94a3b8">GET resolution flow</span><span style="flex:1"></span><button style="font-family:inherit;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#64748b;background:transparent;border:1px solid #1e293b;border-radius:4px;padding:5px 10px;cursor:pointer">copy</button></div><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;line-height:1.3;white-space:pre;color:#cbd5e1;overflow-x:auto"><div><span>  </span><span style="color:#475569">┌</span><span style="color:#475569">────────────</span><span style="color:#475569">┐</span><span>   </span><span style="color:#475569">┌</span><span style="color:#475569">────────────────</span><span style="color:#475569">┐</span><span>   </span><span style="color:#475569">┌</span><span style="color:#475569">────────────────────</span><span style="color:#475569">┐</span><span>   </span><span style="color:#475569">┌</span><span style="color:#475569">────────────</span><span style="color:#475569">┐</span></div><div><span>  </span><span style="color:#475569">│</span><span> client GET </span><span style="color:#475569">│</span><span style="color:#60a5fa">──▶</span><span style="color:#475569">│</span><span style="color:#60a5fa"> nearest region </span><span style="color:#475569">│</span><span style="color:#60a5fa">──▶</span><span style="color:#475569">│</span><span style="color:#4ade80"> local FDB metadata </span><span style="color:#475569">│</span><span style="color:#60a5fa">──▶</span><span style="color:#475569">│</span><span> location   </span><span style="color:#475569">│</span></div><div><span>  </span><span style="color:#475569">└────────────┘</span><span>   </span><span style="color:#475569">└────────────────┘</span><span>   </span><span style="color:#475569">└────────────────────┘</span><span>   </span><span style="color:#475569">└──────┬─────┘</span></div><div style="height:1.3em"></div><div><span>  </span><span style="color:#64748b">choose the byte source</span><span>                                                </span><span style="color:#475569">│</span></div><div><span>             </span><span style="color:#475569">┌───────────────────────┬───────────────────────┬──────────┘</span></div><div><span>             </span><span style="color:#60a5fa">▼</span><span>                       </span><span style="color:#60a5fa">▼</span><span>                       </span><span style="color:#60a5fa">▼</span></div><div><span>  </span><span style="color:#475569">┌</span><span style="color:#475569">─</span><span style="color:#4ade80"> HIT </span><span style="color:#475569">──────────────</span><span style="color:#475569">┐</span><span>  </span><span style="color:#475569">┌</span><span style="color:#475569">─</span><span style="color:#64748b"> LOCAL </span><span style="color:#475569">────────────</span><span style="color:#475569">┐</span><span>  </span><span style="color:#475569">┌</span><span style="color:#475569">─</span><span style="color:#f87171"> MISS </span><span style="color:#475569">─────────────</span><span style="color:#475569">┐</span></div><div><span>  </span><span style="color:#475569">│</span><span style="color:#4ade80"> SSD / block cache  </span><span style="color:#475569">│</span><span>  </span><span style="color:#475569">│</span><span> local block store  </span><span style="color:#475569">│</span><span>  </span><span style="color:#475569">│</span><span style="color:#f87171"> remote source      </span><span style="color:#475569">│</span></div><div><span>  </span><span style="color:#475569">└──────────┬─────────┘</span><span>  </span><span style="color:#475569">└──────────┬─────────┘</span><span>  </span><span style="color:#475569">└──────────┬─────────┘</span></div><div><span>             </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">├</span><span style="color:#f87171">──▶ enqueue warm-cache / move</span></div><div><span>             </span><span style="color:#475569">└───────────────────────┬───────────────────────┘</span></div><div><span>                                     </span><span style="color:#60a5fa">▼</span></div><div><span>                      </span><span style="color:#475569">┌</span><span style="color:#475569">──────────────┬─────────────</span><span style="color:#475569">┐</span></div><div><span>                      </span><span style="color:#475569">│</span><span style="color:#4ade80"> return bytes to client     </span><span style="color:#475569">│</span></div><div><span>                      </span><span style="color:#475569">└────────────────────────────┘</span></div><div style="height:1.3em"></div><div><span>  </span><span style="color:#64748b">// the read can complete before data placement catches up</span></div></pre><div style="font-size:12px;color:#475569;line-height:1.6;text-wrap:pretty;margin-bottom:1rem">Metadata is read in the nearest region; only the bytes travel. A miss serves the read from the remote source and queues the placement work behind it.</div></div>
<p>If the object is in the local block cache, that gets served directly to the
client. If the object is in the local block store, that also gets served
directly to the client. If the data isn't stored locally, Tigris needs to fetch
it from another region. In order to do that it reverse proxies the read to the
block store in the region where the data actually lives. Since a user requested
it, we enqueue a block store replication job so that the next GET request is
faster. This makes future GETs much more efficient.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This is not the case when you are using a
<a href="https://www.tigrisdata.com/blog/multi-region-dual-region-buckets/" target="_blank" rel="noopener noreferrer" class="">dual-region bucket</a>,
which lets you confine objects to a single region for policy or compliance
reasons.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="our-queueing-system">Our queueing system<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#our-queueing-system" class="hash-link" aria-label="Direct link to Our queueing system" title="Direct link to Our queueing system" translate="no">​</a></h2>
<img src="https://www.tigrisdata.com/blog/assets/images/queue-next-to-metadata-fdc8da91500cbf5a462b356202b9e3e8.webp" alt="The queue lives next to the metadata. One transaction commits both the object change and the work that must follow. That is the reliability boundary for background processing. At least once retries the work until the work completes. Tasks are idempotent. Duplicates are safe. Missed work is not. Atomic with metadata. One commit writes the change and enqueues any async tasks. No 'wrote to DB but failed to enqueue' error path. Durable and crash-safe. If a worker dies mid-task its lease expires and another worker picks it up. No lost work.">
<p>We built our own queueing system on top of FoundationDB by following what Apple
did with their
<a href="https://www.foundationdb.org/files/QuiCK.pdf" target="_blank" rel="noopener noreferrer" class="">QuiCK: A Queueing System in CloudKit</a>
paper. The queue is just an ordered FoundationDB keyspace. Workers claim rows
transactionally. The work itself happens asynchronously outside of a
transaction, but the coordination layer is FoundationDB.</p>
<p>Building our own queue on top of FoundationDB lets us ensure that we have ACID
semantics when we add tasks to the queue. This also lets us ensure that we have
at-least-once semantics for tasks in the queue. We cannot lose tasks because
they are always present in the FoundationDB queue.</p>
<p>These task writes are atomic with the metadata so either an object writes
successfully with its tasks and metadata or nothing happens. No in-between
state. An interesting side effect of this is that our workers can crash all they
want. Another worker will pick the task up without any human intervention.</p>
<div data-fig="03" style="display:flex;flex-direction:column;gap:10px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;max-width:52rem;margin-left:auto;margin-right:auto"><div style="display:flex;align-items:baseline;gap:14px"><span style="font-size:11px;letter-spacing:0.16em;color:#f59e0b">FIG 03</span><span style="font-size:13px;color:#94a3b8">control plane, workers, shared FDB</span><span style="flex:1"></span><button style="font-family:inherit;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#64748b;background:transparent;border:1px solid #1e293b;border-radius:4px;padding:5px 10px;cursor:pointer">copy</button></div><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;line-height:1.3;white-space:pre;color:#cbd5e1;overflow-x:auto"><div><span style="color:#475569">┌─</span><span style="color:#64748b"> server / API plane </span><span style="color:#475569">───────────────────┐</span><span>   </span><span style="color:#475569">┌─</span><span style="color:#64748b"> worker deployment </span><span style="color:#475569">──────────────────┐</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌────────────────────────┐</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌─────────────┐</span><span>       </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span> api gateway            </span><span style="color:#475569">│</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span style="color:#f87171"> scheduler 1 </span><span style="color:#475569">│</span><span style="color:#60a5fa">──</span><span style="color:#475569">┬</span><span style="color:#60a5fa">───▶</span><span style="color:#475569">│</span><span> worker 1 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">└────────────┬───────────┘</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">└─────────────┘</span><span>  </span><span style="color:#475569">│</span><span>    </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>              </span><span style="color:#475569">│</span><span>                         </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                  </span><span style="color:#475569">│</span><span>    </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>              </span><span style="color:#60a5fa">▼</span><span>                         </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                  </span><span style="color:#475569">└</span><span style="color:#60a5fa">───▶</span><span style="color:#475569">│</span><span> worker 2 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌────────────────────────┐</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span> transaction layer      </span><span style="color:#475569">│</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">└────────────┬───────────┘</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">│</span><span style="color:#64748b"> worker 3 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>              </span><span style="color:#475569">│</span><span>                         </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>              </span><span style="color:#60a5fa">▼</span><span>                         </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌────────────────────────┐</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌─────────────┐</span><span>       </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span> query processing       </span><span style="color:#475569">│</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span style="color:#60a5fa"> scheduler 2 </span><span style="color:#475569">│</span><span style="color:#60a5fa">──</span><span style="color:#475569">┬</span><span style="color:#60a5fa">───▶</span><span style="color:#475569">│</span><span> worker 4 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">└────────────┬───────────┘</span><span>             </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">└─────────────┘</span><span>  </span><span style="color:#475569">│</span><span>    </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>         </span><span style="color:#475569">┌────┴──────────┐</span><span>              </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                  </span><span style="color:#475569">│</span><span>    </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>         </span><span style="color:#60a5fa">▼</span><span>               </span><span style="color:#60a5fa">▼</span><span>              </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                  </span><span style="color:#475569">└</span><span style="color:#60a5fa">───▶</span><span style="color:#475569">│</span><span> worker 5 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">┌──────────────┐</span><span> </span><span style="color:#475569">┌─────────────┐</span><span>       </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span style="color:#4ade80"> metadata row </span><span style="color:#475569">│</span><span> </span><span style="color:#475569">│</span><span style="color:#f87171"> queue item  </span><span style="color:#475569">│</span><span>       </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">┌──────────┐</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span> </span><span style="color:#475569">└───────┬──────┘</span><span> </span><span style="color:#475569">└──────┬──────┘</span><span>       </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">│</span><span style="color:#64748b"> worker 6 </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>         </span><span style="color:#475569">└─────────────┬─┘</span><span>              </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">└──────────┘</span><span>   </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>                       </span><span style="color:#60a5fa">▼</span><span>                </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>            </span><span style="color:#475569">┌──────────┬─────────┐</span><span>      </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>            </span><span style="color:#475569">│</span><span> commit             </span><span style="color:#475569">│</span><span>      </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>            </span><span style="color:#475569">└──────────┬─────────┘</span><span>      </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>                       </span><span style="color:#475569">│</span><span>                </span><span style="color:#475569">│</span><span>   </span><span style="color:#475569">│</span><span>                                      </span><span style="color:#475569">│</span></div><div><span style="color:#475569">└───────────────────────┬────────────────┘</span><span>   </span><span style="color:#475569">└─────────┴────────────────────────────┘</span></div><div><span>                        </span><span style="color:#475569">│</span><span>                              </span><span style="color:#f87171">▲</span></div><div><span>                        </span><span style="color:#60a5fa">▼</span><span>                              </span><span style="color:#475569">│</span></div><div><span style="color:#475569">┌─</span><span style="color:#64748b"> FOUNDATIONDB </span><span style="color:#475569">────────┬──────────────────────────────┴─────────────────────────────┐</span></div><div><span style="color:#475569">│</span><span>                </span><span style="color:#475569">┌──────┬─────┐</span><span>               </span><span style="color:#475569">┌────────┴───────┐</span><span>                     </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>                </span><span style="color:#475569">│</span><span style="color:#64748b"> data       </span><span style="color:#475569">│</span><span>               </span><span style="color:#475569">│</span><span style="color:#f87171"> queue          </span><span style="color:#475569">│</span><span>                     </span><span style="color:#475569">│</span></div><div><span style="color:#475569">│</span><span>                </span><span style="color:#475569">└────────────┘</span><span>               </span><span style="color:#475569">└────────────────┘</span><span>                     </span><span style="color:#475569">│</span></div><div><span style="color:#475569">└────────────────────────────────────────────────────────────────────────────────────┘</span></div><div style="height:1.3em"></div><div><span style="color:#64748b">// one transaction writes the metadata row and the queue item together</span></div></pre><div style="font-size:12px;color:#475569;line-height:1.6;text-wrap:pretty;margin-bottom:1rem">The API plane never talks to workers directly. One transaction writes the metadata row and the queue item; schedulers lease work out of the same keyspace.</div></div>
<p>Our global replication system is built on the back of this queue, meaning that
you can list objects anywhere in any region and you will get the same view of
your bucket in every region. This is all driven by our queue.</p>
<p>When a user does a read on an object and there isn't a copy of it locally, this
async queueing system automatically kicks in to make sure that the data is
copied over so it's hot and ready for next time. Future reads can just happen
from the cache on that particular region.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>FoundationDB is more than just our metadata storage layer, it's the <em>foundation</em>
to the Tigris platform. All objects, indices, and tasks use FoundationDB as the
bedrock for storage. This lets us have data propagate everywhere in the world
without having to build a multi-region FoundationDB cluster or add an expensive
second coordination system into our stack. Every region has their own
FoundationDB clusters that are all kept in sync automatically.</p>
<p>Storing metadata, indices, and task queues in the same database lets us focus
more of our time on delivering users global object storage and less of our time
fiddling with the details on how to make Kafka performant. Without FoundationDB
we would not have the primitives we need to make Tigris happen.</p>
<p>And that's it, thank you!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="further-reading">Further reading<a href="https://www.tigrisdata.com/blog/fdb-krea-talk/#further-reading" class="hash-link" aria-label="Direct link to Further reading" title="Direct link to Further reading" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.tigrisdata.com/blog/building-a-database-using-foundationdb/" target="_blank" rel="noopener noreferrer" class="">Skipping the boring parts of building a storage platform using FoundationDB</a>
— why we didn't build our own distributed transaction layer.</li>
<li class=""><a href="https://www.tigrisdata.com/blog/data-layer-foundationdb/" target="_blank" rel="noopener noreferrer" class="">Tigris metadata layer on FoundationDB</a>
— the key encoding, integer ID compression, and value format in detail.</li>
<li class=""><a href="https://www.tigrisdata.com/blog/backing-up-foundationdb/" target="_blank" rel="noopener noreferrer" class="">Backing up FoundationDB</a></li>
</ul>]]></content:encoded>
            <category>Engineering</category>
            <category>FoundationDB</category>
            <category>Object Storage</category>
            <category>distributed systems</category>
        </item>
        <item>
            <title><![CDATA[Extending immutability: deletion without losing data]]></title>
            <link>https://www.tigrisdata.com/blog/soft-delete-deep-dive/</link>
            <guid>https://www.tigrisdata.com/blog/soft-delete-deep-dive/</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Deleting data is hard in a geo-replicated active-active database. Here's how Tigris built a Recycle Bin for objects and buckets on top of immutable storage.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-94060fe7c50c7ca120ec67b17e43c05b.webp" class="hero-image" alt="A blue tiger leaps down from dark rocky cliffs toward a storm-tossed sea, reaching for a galvanized bucket floating among the waves alongside VHS tapes with their ribbon unspooling, scattered Polaroid photographs, a manila folder labeled DOCUMENTS, loose typed pages, and brightly coloured polyhedral dice">
<p>Tigris has a pretty advanced replication scheme for writes. What happens when
you actually need to delete things? Turns out deleting things is hard in
distributed systems. Especially when you have a geo-replicated active-active
database like Tigris does. We can (and do) use tombstones to mark where data
once was, but how do you let people undo an accidental delete?</p>
<p>Tigris wants to turn storage
<a href="https://martin.kleppmann.com/2015/03/04/turning-the-database-inside-out.html" target="_blank" rel="noopener noreferrer" class="">inside out</a>,
so our implementation of soft deletion is by giving users the Recycle Bin for
objects and buckets. Today we’re going to dig into how this works, why it works,
and what this gives you in terms of using object storage today.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="recycle-bins-and-you">Recycle bins and you<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#recycle-bins-and-you" class="hash-link" aria-label="Direct link to Recycle bins and you" title="Direct link to Recycle bins and you" translate="no">​</a></h2>
<p>In Windows and macOS, the Recycle Bin (or Trash can) is a form of purgatory
where deleted files wait for their storage to be deallocated by the user. This
allows users to hit “delete” fearlessly because if they made a mistake they can
just drag it back out and go on with life.</p>
<p>This works great in your local filesystem because there’s only one writer in one
region. This kinda falls apart when you have multiple regions in your database
and any one of them could be writing to it. How do you name things in the
recycle bin? How do you handle the conflict of an update happening in one region
before the deletion was fully replicated out from another region?</p>
<p>This is the fun of distributed systems, which is the kind of problem space that
Tigris lives in.</p>
<p>One way to think about how the Recycle Bin works is that the file metadata gets
moved there when the user hits delete. No data bytes move around on the disk,
but the file doesn’t show up in My Documents anymore. In a distributed systems
context you can’t just move the metadata around, you have to leave a tombstone
behind to record where that metadata once was. This prevents other regions from
being confused when actions happen really close to each other in time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="soft-deletes-in-some-universes">Soft deletes in some universes<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#soft-deletes-in-some-universes" class="hash-link" aria-label="Direct link to Soft deletes in some universes" title="Direct link to Soft deletes in some universes" translate="no">​</a></h2>
<p>At a high level, a <a href="https://www.metabase.com/glossary/soft-delete" target="_blank" rel="noopener noreferrer" class="">soft-delete</a>
is when a DELETE action doesn’t actually remove the data. When data is
soft-deleted, it’s still there but just not visible in the main usage flow. This
lets you get the data back when a delete is made by accident.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-database-becomes-your-api">Your database becomes your API<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#your-database-becomes-your-api" class="hash-link" aria-label="Direct link to Your database becomes your API" title="Direct link to Your database becomes your API" translate="no">​</a></h3>
<p>One of the interesting side effects of designing any API is that you end up
leaking the internals of how your database works to your users. Many object
storage systems were designed with overwriting or deleting data as one of the
primary operations, and as such have had to bolt versioning onto the side. For
the most part this does work; but once you get into advanced versioning schemes
everything starts to fall apart. Tigris doesn’t suffer from the same problems
because we built immutability into the core from day one, and in immutable
systems you have to append new data on the end instead of overwriting data.</p>
<p>At the least, actually storing the data en masse is a boring problem. You put
the data somewhere, maybe name it after the checksum of its contents, and then
have a daemon make sure it’s copied three places. That daemon also handles cases
when drives go offline and new ones are added to make sure data is shuffled
around the cluster. This is largely a solved problem with projects like Ceph,
Longhorn, or other distributed storage systems.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="s3-uses-delete-markers">S3 uses delete markers<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#s3-uses-delete-markers" class="hash-link" aria-label="Direct link to S3 uses delete markers" title="Direct link to S3 uses delete markers" translate="no">​</a></h3>
<p>Some object storage systems like S3 expose platform internals to make soft
deletion work. In S3 deleting an object creates a delete marker (tombstone). A
delete marker is an explicit marker that the object is deleted and should not be
returned in normal operation. Here’s what that looks like in practice:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 01</span><span style="font-size:12px;color:#94a3b8">DeleteObject writes a delete marker</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#475569">┌───────────────────┐  ┌───────────────────┐  </span><span style="color:#f87171">┌───────────────────┐ ◀── DeleteObject</span></div><div>  <span style="color:#475569">│</span><span style="color:#64748b"> v1                </span><span style="color:#475569">│  │</span><span style="color:#64748b"> v2                </span><span style="color:#475569">│  </span><span style="color:#f87171">│</span><span style="color:#64748b"> v3        </span><span style="color:#f87171">current │</span></div><div>  <span style="color:#475569">│</span> report.pdf        <span style="color:#475569">│  │</span> report.pdf        <span style="color:#475569">│  </span><span style="color:#f87171">│</span> report.pdf        <span style="color:#f87171">│</span></div><div>  <span style="color:#475569">│</span><span style="color:#64748b"> …198086           </span><span style="color:#475569">│  │</span><span style="color:#64748b"> …198088           </span><span style="color:#475569">│  </span><span style="color:#f87171">│ delete marker     │</span></div><div>  <span style="color:#475569">└─────────┬─────────┘  └─────────┬─────────┘  </span><span style="color:#f87171">└───────────────────┘</span></div><div>  <span style="color:#60a5fa">          │                      │              </span><span style="color:#f87171">no data</span></div><div style="color:#60a5fa">            ▼                      ▼</div><div style="color:#475569">  ┌───────────────────────────────────────────────────────────────────┐</div><div>  <span style="color:#475569">│</span><span style="color:#64748b"> sea of data                                                       </span><span style="color:#475569">│</span></div><div style="color:#475569">  │    ┌──────────┐         ┌──────────┐                              │</div><div>  <span style="color:#475569">│    │</span> v1 bytes <span style="color:#475569">│         │</span> v2 bytes <span style="color:#475569">│                              │</span></div><div style="color:#475569">  │    └──────────┘         └──────────┘                              │</div><div style="color:#475569">  └───────────────────────────────────────────────────────────────────┘</div><div> </div><div style="color:#64748b">  // the bytes stay. only the newest record says the object is gone.</div></pre></figure></div>
<p>I don’t know how I feel about this flow. Based on reading between the lines in
the
<a href="https://docs.aws.amazon.com/en_es/AmazonS3/latest/userguide/ManagingDelMarkers.html" target="_blank" rel="noopener noreferrer" class="">delete marker documentation</a>
it really feels like this is a leaked internal implementation detail of how S3’s
eventually consistent database works instead of a full fledged feature of the
storage system. If I had to choose between leaking internal database details in
the API and implementing a higher level API for something complicated like soft
deletion, I’d want to implement the higher level API.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="tigris-soft-deletes-are-external-references">Tigris’ soft deletes are external references<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#tigris-soft-deletes-are-external-references" class="hash-link" aria-label="Direct link to Tigris’ soft deletes are external references" title="Direct link to Tigris’ soft deletes are external references" translate="no">​</a></h3>
<p>Let’s rethink what soft deletes really are. What if they were like the Recycle
Bin in Windows?</p>
<p>Soft-deletes are external references to buckets or objects that live in a
different namespace from normal buckets or objects. We implemented them as
external references instead of tombstones because this is effectively moving
object metadata to the recycle bin. Tombstones mark the data as not being there,
but soft-delete markers are a copy of the data that was there. This makes it
easy to put the object back in place if you deleted it by mistake.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="garbage-collection-roots">Garbage collection roots<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#garbage-collection-roots" class="hash-link" aria-label="Direct link to Garbage collection roots" title="Direct link to Garbage collection roots" translate="no">​</a></h3>
<p>One way to think about objects and buckets is that they are garbage collection
roots for points in the endless sea of data. Any data in the sea without a root
anchoring it down is eligible to be deleted. Uploading multiple versions of an
object with a forkable bucket creates multiple metadata entries at their
different timestamped version numbers. You can then fork a bucket from any one
of those timestamps to see what the bucket was like at that point:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 02</span><span style="font-size:12px;color:#94a3b8">fork at any version timestamp to see the bucket's past</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">every write appends a new version entry</span></div><div> </div><div>  <span style="color:#4ade80">v1</span>              <span style="color:#4ade80">v2</span>              <span style="color:#4ade80">╎</span>   <span style="color:#475569">v3</span>              <span style="color:#475569">v4</span></div><div>  <span style="color:#4ade80">┌────────────┐</span>  <span style="color:#4ade80">┌────────────┐</span>  <span style="color:#4ade80">╎</span>   <span style="color:#475569">┌────────────┐</span>  <span style="color:#475569">┌────────────┐</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">…198086</span>    <span style="color:#4ade80">│</span>  <span style="color:#4ade80">│</span> <span style="color:#64748b">…431907</span>    <span style="color:#4ade80">│</span>  <span style="color:#4ade80">╎</span>   <span style="color:#475569">│</span> <span style="color:#64748b">…764522</span>    <span style="color:#475569">│</span>  <span style="color:#475569">│</span> <span style="color:#64748b">…055310</span>    <span style="color:#475569">│</span></div><div>  <span style="color:#4ade80">│</span> 4.1 MB     <span style="color:#4ade80">│</span>  <span style="color:#4ade80">│</span> 4.3 MB     <span style="color:#4ade80">│</span>  <span style="color:#4ade80">╎</span>   <span style="color:#475569">│</span> 4.4 MB     <span style="color:#475569">│</span>  <span style="color:#475569">│</span> 5.0 MB     <span style="color:#475569">│</span></div><div>  <span style="color:#4ade80">└─────┬──────┘</span>  <span style="color:#4ade80">└─────┬──────┘</span>  <span style="color:#4ade80">╎</span>   <span style="color:#475569">└─────┬──────┘</span>  <span style="color:#475569">└─────┬──────┘</span></div><div>  <span style="color:#475569">──────┴───────────────┴─────────</span><span style="color:#4ade80">╎</span><span style="color:#475569">─────────┴───────────────┴───────▶</span></div><div>  <span style="color:#64748b">earlier</span>                         <span style="color:#4ade80">╎ fork point</span>                <span style="color:#64748b">later</span></div><div>  <span style="color:#4ade80">the fork inherits these</span>         <span style="color:#4ade80">╎</span>   <span style="color:#64748b">written later — the fork never sees them</span></div><div>                                  <span style="color:#4ade80">▼</span></div><div>                      <span style="color:#4ade80">┌──────────────────────────────┐</span></div><div>                      <span style="color:#4ade80">│</span> uploads/report.pdf           <span style="color:#4ade80">│</span></div><div>                      <span style="color:#4ade80">│</span> <span style="color:#64748b">current version</span> <span style="color:#4ade80">v2</span>           <span style="color:#4ade80">│</span></div><div>                      <span style="color:#4ade80">│</span> <span style="color:#64748b">as of 1775929812004431907</span>    <span style="color:#4ade80">│</span></div><div>                      <span style="color:#4ade80">└──────────────────────────────┘</span></div><div> </div><div>  <span style="color:#64748b">// appending metadata instead of overwriting keeps any past instant addressable.</span></div></pre></figure></div>
<p>This would solve the soft-delete problem, but our existing database schema using
FoundationDB requires us to enable forking and snapshots at bucket creation
time. In essence, we need something that’s halfway between what we have (each
bucket being a globally mutable namespace) and the bucket forking land of every
action being appending metadata onto the end.</p>
<p>To do that, we basically implemented most of that appending metadata on the end
trick but to a different place: the soft deletion corner. When you enable
soft-deletion and delete an object, its metadata gets moved to the trashcan so
you can pluck it back into place:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 03</span><span style="font-size:12px;color:#94a3b8">delete moves the metadata record to the soft-delete keyspace</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">main table · live keyspace</span>                <span style="color:#64748b">soft-delete keyspace · newest first</span></div><div>  <span style="color:#4ade80">┌────────────────────────────┐</span>            <span style="color:#f59e0b">┌──────────────────────────────────┐</span></div><div>  <span style="color:#4ade80">│</span> uploads/report.pdf         <span style="color:#4ade80">│</span>            <span style="color:#f59e0b">│</span> uploads/report.pdf   <span style="color:#64748b">3rd delete</span>  <span style="color:#f59e0b">│</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">live metadata record</span>       <span style="color:#4ade80">│</span><span style="color:#f59e0b">───────▶</span>    <span style="color:#f59e0b">│</span> <span style="color:#64748b">deleted …768707198086</span>            <span style="color:#f59e0b">│</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">in ListObjectsV2 output</span>    <span style="color:#4ade80">│</span><span style="color:#4ade80">◀╌╌╌╌╌╌</span>     <span style="color:#f59e0b">└──────────────────────────────────┘</span></div><div>                                            <span style="color:#f59e0b">┌──────────────────────────────────┐</span></div><div>                                            <span style="color:#f59e0b">│</span> uploads/report.pdf   <span style="color:#64748b">2nd delete</span>  <span style="color:#f59e0b">│</span></div><div>                                            <span style="color:#f59e0b">│</span> <span style="color:#64748b">deleted …412888100731</span>            <span style="color:#f59e0b">│</span></div><div>  <span style="color:#475569">┌────────────────────────────┐</span>            <span style="color:#f59e0b">└──────────────────────────────────┘</span></div><div>  <span style="color:#475569">│</span> uploads/notes.md           <span style="color:#475569">│</span>            <span style="color:#f59e0b">┌──────────────────────────────────┐</span></div><div>  <span style="color:#475569">│</span> <span style="color:#64748b">untouched by the delete</span>    <span style="color:#475569">│</span>            <span style="color:#f59e0b">│</span> uploads/report.pdf   <span style="color:#64748b">1st delete</span>  <span style="color:#f59e0b">│</span></div><div>  <span style="color:#475569">└────────────────────────────┘</span>            <span style="color:#f59e0b">│</span> <span style="color:#64748b">deleted …104233715492</span>            <span style="color:#f59e0b">│</span></div><div>                                            <span style="color:#f59e0b">└──────────────────────────────────┘</span></div><div> </div><div>  <span style="color:#f59e0b">───────▶</span>  <span style="color:#64748b">DeleteObject moves the record out — one entry per delete</span></div><div>  <span style="color:#4ade80">◀╌╌╌╌╌╌</span>   <span style="color:#64748b">RestoreObject moves the same metadata back</span></div><div> </div><div>  <span style="color:#64748b">// the bin entry is a copy of the metadata, not a marker. restoring is a move.</span></div></pre></figure></div>
<p>It’s the same basic idea as the recycle bin on your desktop. Any buckets or
objects left in the recycle bin for long enough become eligible to be deleted,
which then makes the backend go and securely erase things. Effectively, any bits
of metadata in the soft deletion corner are still considered garbage collection
roots, they’re just not shown when you do a normal <code>ListObjectsV2</code> call.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="distributed-systems-are-fun">Distributed systems are fun*<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#distributed-systems-are-fun" class="hash-link" aria-label="Direct link to Distributed systems are fun*" title="Direct link to Distributed systems are fun*" translate="no">​</a></h3>
<p>The real fun comes into play when you remember that Tigris has a globally
replicated active-active database where any region can change any object at any
time. Most of the time things work out and objects are replicated without too
much strife. The annoying part comes when two events are ordered weirdly.
Imagine a scenario where one agent in one datacentre deletes an object <em>after</em>
another agent in another datacentre:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 04</span><span style="font-size:12px;color:#94a3b8">two regions write to one key at the same time</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">ORD (Chicago)</span>                               <span style="color:#64748b">IAD (Ashburn)</span></div><div>  <span style="color:#4ade80">┌──────────────────────────┐</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#4ade80">PutObject</span>  <span style="color:#64748b">· agent A</span>     <span style="color:#4ade80">│</span></div><div>  <span style="color:#4ade80">│</span> uploads/report.pdf       <span style="color:#4ade80">│</span>  <span style="color:#60a5fa">──── replicates PUT ────▶</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">t = …768707198086</span>        <span style="color:#4ade80">│</span></div><div>  <span style="color:#4ade80">└──────────────────────────┘</span></div><div> </div><div>                                              <span style="color:#f87171">┌──────────────────────────┐</span></div><div>                                              <span style="color:#f87171">│</span> <span style="color:#f87171">DeleteObject</span>  <span style="color:#64748b">· agent B</span>  <span style="color:#f87171">│</span></div><div>  <span style="color:#60a5fa">◀── replicates DELETE ──</span>                    <span style="color:#f87171">│</span> uploads/report.pdf       <span style="color:#f87171">│</span></div><div>                                              <span style="color:#f87171">│</span> <span style="color:#64748b">t = …768984210773</span>        <span style="color:#f87171">│</span></div><div>                                              <span style="color:#f87171">└──────────────────────────┘</span></div><div> </div><div>  <span style="color:#64748b">ORD applies</span>   PUT  ▸  DELETE    <span style="color:#64748b">deleted, as expected</span></div><div>  <span style="color:#64748b">IAD applies</span>   DELETE  ▸  PUT    <span style="color:#f59e0b">the put looks brand new</span></div><div> </div><div>  <span style="color:#64748b">// two writers, one key. the regions disagree about whether it exists.</span></div></pre></figure></div>
<p>How would this replicate out? Well for one each change is timestamped by when
it’s done in terms of Unix nanoseconds, so the replication messages kinda look
like this:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 05</span><span style="font-size:12px;color:#94a3b8">replication records are timestamped in Unix nanoseconds</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">produced first</span>                          <span style="color:#64748b">produced 277 ms later</span></div><div>  <span style="color:#4ade80">┌──────────────────────────────────┐</span>    <span style="color:#f87171">┌──────────────────────────────────┐</span></div><div>  <span style="color:#4ade80">│</span> uploads/report.pdf               <span style="color:#4ade80">│</span>    <span style="color:#f87171">│</span> uploads/report.pdf               <span style="color:#f87171">│</span></div><div>  <span style="color:#4ade80">│</span> op: <span style="color:#4ade80">PUT</span>                          <span style="color:#4ade80">│</span>    <span style="color:#f87171">│</span> op: <span style="color:#f87171">DELETE</span>                       <span style="color:#f87171">│</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">LastModified</span>  1775929768<span style="color:#4ade80">707198086</span><span style="color:#4ade80">│</span>    <span style="color:#f87171">│</span> <span style="color:#64748b">LastModified</span>  1775929768<span style="color:#f87171">984210773</span><span style="color:#f87171">│</span></div><div>  <span style="color:#4ade80">│</span> <span style="color:#64748b">block 0x3f2ac701 · origin ORD</span>    <span style="color:#4ade80">│</span>    <span style="color:#f87171">│</span> <span style="color:#64748b">block 0x3f2ac701 · origin IAD</span>    <span style="color:#f87171">│</span></div><div>  <span style="color:#4ade80">└──────────────────────────────────┘</span>    <span style="color:#f87171">└──────────────────────────────────┘</span></div><div> </div><div>  <span style="color:#64748b">last write wins</span></div><div>  <span style="color:#f87171">984210773</span>   <span style="color:#64748b">is greater than</span>  <span style="color:#4ade80">707198086</span></div><div> </div><div>  <span style="color:#64748b">// the delete carries the newer LastModified, so the delete survives</span></div></pre></figure></div>
<p>This means that in theory, a user could DELETE an object <em>before</em> an update is
processed by another region, and that would make the regions disagree about if
the object exists or not. This is a horrible state to be in and usually requires
support intervention or to recreate/re-delete the object.</p>
<p>The root cause boils down to deleting objects actually deleting metadata from
the database doesn’t scale past a single region. Updates to metadata include the
entire metadata object, so if you delete it locally and a new version is pushed
remotely, the object will gain the remote state.</p>
<p>We don’t want users to have to deal with that, so we added the concept of
anti-resurrection to Tigris. Any write to an object must prove it is newer than
the deletion.</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 06</span><span style="font-size:12px;color:#94a3b8">the tombstone is what a stale write must beat</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">ORD · agent PutObject</span>                     <span style="color:#64748b">IAD · user DeleteObject</span></div><div>  <span style="color:#475569">┌──────────────────────────┐</span>              <span style="color:#f87171">┌──────────────────────────┐</span></div><div>  <span style="color:#475569">│</span> uploads/report.pdf       <span style="color:#475569">│</span>              <span style="color:#f87171">│</span> uploads/report.pdf       <span style="color:#f87171">│</span></div><div>  <span style="color:#475569">│</span> <span style="color:#64748b">new version · v2</span>         <span style="color:#475569">│</span><span style="color:#f87171">◀─────────────</span><span style="color:#f87171">│</span> <span style="color:#64748b">row deleted · marker kept</span><span style="color:#f87171">│</span></div><div>  <span style="color:#475569">│</span> <span style="color:#64748b">t = 15</span>                   <span style="color:#475569">│</span>   <span style="color:#f87171">delete</span>     <span style="color:#f87171">│</span> <span style="color:#f87171">tombstone t = 25</span>         <span style="color:#f87171">│</span></div><div>  <span style="color:#475569">└────────────┬─────────────┘</span>              <span style="color:#f87171">└────────────┬─────────────┘</span></div><div>               <span style="color:#60a5fa">└────────────────────┬────────────────────┘</span></div><div>                                    <span style="color:#60a5fa">▼</span></div><div>    <span style="color:#4ade80">┌────────────────────────────────────────────────────────────┐</span></div><div>    <span style="color:#4ade80">│</span> at IAD: is the write newer than the marker?                <span style="color:#4ade80">│</span></div><div>    <span style="color:#4ade80">│</span> <span style="color:#64748b">write t = 15  ·  marker t = 25  ·  is 15 &gt; 25 ?</span>            <span style="color:#4ade80">│</span></div><div>    <span style="color:#4ade80">│</span> <span style="color:#f87171">no — not strictly newer, so the write is dropped</span>           <span style="color:#4ade80">│</span></div><div>    <span style="color:#4ade80">│</span> <span style="color:#64748b">equal timestamps lose too · the guard runs for every bucket</span><span style="color:#4ade80">│</span></div><div>    <span style="color:#4ade80">└────────────────────────────────────────────────────────────┘</span></div><div> </div><div>  <span style="color:#64748b">// without the marker, an empty slot looks exactly like a key that never existed.</span></div></pre></figure></div>
<p>In this circumstance, a user sent a DeleteObject request to the IAD datacentre
at time 25, but an agent sent a new version of the object with PutObject to the
ORD datacentre at time 15. The user’s delete is newer than the agent’s put, so
the new version is rejected and the delete gets sent back to ORD.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="using-soft-deletes">Using soft deletes<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#using-soft-deletes" class="hash-link" aria-label="Direct link to Using soft deletes" title="Direct link to Using soft deletes" translate="no">​</a></h2>
<p>Tigris extends the S3 API by having users add headers to their requests. For
example, to create a bucket with soft deletion enabled:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"context"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"github.com/aws/aws-sdk-go-v2/aws"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"github.com/aws/aws-sdk-go-v2/service/s3"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"github.com/tigrisdata/storage-go"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithGlobalEndpoint</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithAccessKeypair</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		os</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Getenv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"TIGRIS_STORAGE_ACCESS_KEY_ID"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		os</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Getenv</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"TIGRIS_STORAGE_SECRET_ACCESS_KEY"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">CreateBucketWithSoftDelete</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CreateBucketWithSoftDeleteInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	CreateBucketInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CreateBucketInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	RetentionDays</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">     </span><span class="token number">30</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// 0 uses the 7-day default</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Or to list soft-deleted objects:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">out</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ListSoftDeletedObjects</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">ListSoftDeletedObjectsInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	Prefix</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"uploads/"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> err</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> </span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> o </span><span class="token operator">:=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">range</span><span class="token plain"> out</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Objects </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Printf</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"%s v=%s %d bytes deleted=%s"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> o</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> o</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">VersionID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> o</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Size</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> o</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">LastModified</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Or to permanently delete one soft-deleted version:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">PermanentlyDeleteObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"uploads/report.pdf"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"1775929768707198086"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>When you have a soft-delete enabled bucket, you can also forcibly delete an
entire bucket:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ForceDeleteBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">DeleteBucketInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>warning</div><div class="admonitionContent_BuS1"><p>If you use this call on a bucket that doesn’t have soft deletion enabled, you
have permanently deleted your bucket. Please call this with care. Support cannot
help you if you use this call wrongly.</p></div></div>
<p>And then bring it back from the dead:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">trash</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ListSoftDeletedBuckets</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> err</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> </span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> b </span><span class="token operator">:=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">range</span><span class="token plain"> trash</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Buckets </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Printf</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"%s (%d day retention)"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">RetentionDays</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">RestoreBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">RestoreBucketInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> err</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="now-what">Now what?<a href="https://www.tigrisdata.com/blog/soft-delete-deep-dive/#now-what" class="hash-link" aria-label="Direct link to Now what?" title="Direct link to Now what?" translate="no">​</a></h2>
<p>Object storage entered our stacks as an unlimited FTP server we all used for
backups. A distressing amount of the world’s most important data lives in object
storage buckets because it’s the best place to put it. This is why having an
“undo” button matters, it’s what makes it safe to trust your backups in the
cloud. To err is human, and mistakes are a “when” to plan for, not an “if” that
you hopefully never have happen. The blast radius of one overly wide
<code>--recursive</code> flag is measured in years of people’s lives.</p>
<p>One of the biggest usecases that comes to mind is ransomware prevention. Imagine
a case where an attacker downloads everything in your bucket, deletes it, and
asks for a ransom to send you the files back. With Tigris, soft deletes means
that the ransom can be ignored, you can un-delete your data, and be on your
merry way with incident response. The other big usecase is for agents, where
they somehow get the idea that deleting production data is the right way to
solve a problem. Both cases mean you need a quick and fast way to go back to
before things went wrong.</p>
<p>If you want true isolation instead of recovery, that’s why we have
<a href="https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/" target="_blank" rel="noopener noreferrer" class="">bucket forking</a>.
Bucket forking needs to be enabled before a bucket is created, but you can
enable soft deletion on any bucket in the dashboard whenever you want.</p>
<p>Every storage system is going to make you choose between ones that hide how the
platform works and ones that expose the gorey internals to users. I think that
hiding the internals and exposing the high level operations built on top of them
is the right way to go, if only because the higher level operations are much
easier to make safe in our globally distributed future.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Turn on the undo button</span><p>Enable soft delete on any Tigris bucket, new or existing, and every delete becomes recoverable for up to 90 days. Restoring a whole bucket is one call.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/buckets/soft-delete/" class="cta-link"><div>Read the soft delete docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>Object Storage</category>
            <category>Soft Delete</category>
            <category>distributed systems</category>
            <category>foundationdb</category>
        </item>
        <item>
            <title><![CDATA[SigV4 authentication is surprisingly complicated]]></title>
            <link>https://www.tigrisdata.com/blog/sigv4/</link>
            <guid>https://www.tigrisdata.com/blog/sigv4/</guid>
            <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[SigV4 looks simple: sign a request, check the signature. Then you implement
canonicalization, clock skew, and a cache that isn't allowed to hold your key.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-bb06c06ac938f9eeec2b6b1271ce92a0.webp" class="hero-image" alt="A blue tiger in a machinist's visor cuts a key on a bench-mounted duplicating machine, in a dim workshop whose walls are covered floor to ceiling with hundreds of brass keys hanging on rings.">
<p>Tigris is a drop-in replacement for AWS S3 (or GCS, anything S3API compatible).
As such, we need to be fully compatible with both the mechanisms and semantics
of S3 including the
<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html" target="_blank" rel="noopener noreferrer" class="">SigV4 authentication protocol</a>.
This is the lingua franca of authentication in the object storage landscape;
even Google Cloud Storage has a way to enable SigV4 support so you can use
existing applications against its object storage service.</p>
<p>At first I thought that SigV4 was fairly simple. Clients sign requests, servers
do the same work and make sure the result matches. The main sticking point is
that the cryptography involved is symmetric cryptography, the kind where both
parties need to have the same secrets. This makes some scaling issues weird, but
we'll get into that in the future.</p>
<!-- -->
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This is only going to be talking about <em>authentication</em> (ensuring the identity
of a remote client), not <em>authorization</em> (ensuring the client has the permission
to do something).</p><p>Authorization will come in the future for reasons that will become obvious when
you see that post. We basically needed to implement a compiler. That is not a
typo.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sigv4-in-a-shellnut">SigV4 in a shellnut<a href="https://www.tigrisdata.com/blog/sigv4/#sigv4-in-a-shellnut" class="hash-link" aria-label="Direct link to SigV4 in a shellnut" title="Direct link to SigV4 in a shellnut" translate="no">​</a></h2>
<p>At a high level when a client signs a request with SigV4 you get an access key
ID and secret access key. The access key ID is functionally a username and the
secret access key is functionally a password. Admins can identify keypairs by
the access key ID (without special training or tools) and services use the owner
of the access key or policies delegated to that access key to determine what
actions that client may take.</p>
<p>SigV4 uses
<a href="https://en.wikipedia.org/wiki/HMAC" target="_blank" rel="noopener noreferrer" class="">HMAC (hash-based Message Authentication Code)</a>
and
<a href="https://en.wikipedia.org/wiki/SHA-2" target="_blank" rel="noopener noreferrer" class="">SHA-256 (SHA-2 with a 256 bit hash width)</a>
to do authentication by creating salted hashes based on request metadata.</p>
<p>In order to send a SigV4 request, clients take the outgoing request, reduce it
to a canonicalized form, and sign it with a symmetric key derived from the
secret access key, the current date, region of the service, and service name,
kinda like this Go code:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> data </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token builtin" style="color:rgb(189, 147, 249)">byte</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token builtin" style="color:rgb(189, 147, 249)">byte</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	h </span><span class="token operator">:=</span><span class="token plain"> hmac</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">sha256</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	h</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Write</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> h</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Sum</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token boolean">nil</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">var</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	kDate    </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"AWS4"</span><span class="token operator">+</span><span class="token plain">secretAccessKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> nowDate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	kRegion  </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kDate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> region</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	kService </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kRegion</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> service</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	kSigning </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kService</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"aws4_request"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>As an example, let's see what a signed <code>GET</code> request to a
<a href="https://github.com/Xe/x/blob/master/cmd/httpdebug/main.go" target="_blank" rel="noopener noreferrer" class="">HTTP debugging endpoint</a>
looks like on the wire with and without the signature:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$ curl http://localhost:3000 -v</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET /</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">User-Agent: curl/8.7.1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Accept: */*</span><br></div></code></pre></div></div>
<p>And when you add the signature with
<a href="https://how.wtf/aws-sigv4-requests-with-curl.html" target="_blank" rel="noopener noreferrer" class=""><code>--aws-sigv4</code></a>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$ curl \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --user tid_YOISC719YLXSONFU:tsec_DiYqeH8t0IKjKUKfqhzTsqrCCUl9Wm0m+6MXNhhi1fU \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --aws-sigv4 aws:amz:auto:s3 \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  -v \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  http://localhost:3000</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET /</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">User-Agent: curl/8.7.1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Accept: */*</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Authorization:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  AWS4-HMAC-SHA256</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Credential=tid_YOISC719YLXSONFU/20260720/auto/s3/aws4_request,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  SignedHeaders=host;x-amz-date,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Signature=879bcdd43749cfc9782b876d9ceb3ff153d79ab1482290cca7ab915bb7f8785d</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">X-Amz-Date: 20260720T153748Z</span><br></div></code></pre></div></div>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This is not a live keypair, it was specifically crafted for this post.</p></div></div>
<p>Breaking it down we have two extra headers in the request:</p>
<ul>
<li class=""><code>Authorization</code>: The fixed string <code>AWS4-HMAC-SHA256</code> to signal to the server
which authentication mechanism is in use. The rest of the string is
information about the request signature so the server can properly
canonicalize the request.</li>
<li class=""><code>X-Amz-Date</code>: The date and time (UTC) of the request so the server knows
<em>when</em> the request was signed. Servers will use this request date in order to
reject old requests to prevent
<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/" target="_blank" rel="noopener noreferrer" class="">replay attacks</a>.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="request-canonicalization-and-signing">Request canonicalization and signing<a href="https://www.tigrisdata.com/blog/sigv4/#request-canonicalization-and-signing" class="hash-link" aria-label="Direct link to Request canonicalization and signing" title="Direct link to Request canonicalization and signing" translate="no">​</a></h3>
<p>On the wire, HTTP/1.1 requests look kinda like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET /api/list?page=0&amp;count=30</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">User-Agent: curl/8.7.1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Accept: */*</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Host: myawesomesite.example</span><br></div></code></pre></div></div>
<p>However the headers could be sent in any order, and changing the order of
request headers doesn't result in different requests. Additionally any query
string parameters could be formatted in any way a client (or server) could
imagine, including
<a href="https://github.com/TecharoHQ/anubis/issues/1644" target="_blank" rel="noopener noreferrer" class="">the use of semicolons to separate values</a>.
All attempts to canonicalise HTTP requests <em>MUST</em> deal with this ambiguity and
define their own rules.</p>
<p>SigV4 canonical requests are made up of a few parts:</p>
<ul>
<li class="">The HTTP method (<code>GET</code>, <code>PUT</code>, <code>POST</code>, <code>DELETE</code>, etc.)</li>
<li class="">The URI path of the request (<code>/api/list</code>, etc.)</li>
<li class="">The sorted canonical query string (you must exactly match the server-side
canonicalization logic)</li>
<li class="">The signed headers terminated with two newlines</li>
<li class="">The sorted list of signed headers joined by semicolons</li>
<li class="">The SHA256 checksum of the request body</li>
</ul>
<p>For that example <code>/api/list</code> request, the canonical form would look like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">/api/list</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">count=30&amp;page=0</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">host:myawesomesite.example</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">x-amz-date:20260715T204745Z</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">host;x-amz-date</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</span><br></div></code></pre></div></div>
<p>As the request has no body, the empty sha256 checksum
<code>e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</code> is put as the
body checksum.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This exact approach requires clients and services to buffer the entire request
body before processing it. There is a subset of SigV4 that supports
arbitrary-sized bodies without having to buffer the entire request using
<code>STREAMING-AWS4-HMAC-SHA256-PAYLOAD</code>, which requires extra logic that is way out
of scope for now.</p><p>If you want to learn more, give your favourite AI agent the following prompt:</p><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">I'm reading the blogpost at &lt;link&gt; and Xe mentioned AWS SigV4's</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">STREAMING-AWS4-HMAC-SHA256-PAYLOAD method. I would like to learn more about how</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">this works. Please research how this works and give me code and request body</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">samples.</span><br></div></code></pre></div></div><p>Additionally, when you are doing presigned URL uploads in object storage, you
replace the body hash with the fixed string <code>UNSIGNED-PAYLOAD</code> when
canonicalizing because you have no way of knowing what data the client will
upload or what the SHA256 checksum will be.</p></div></div>
<p>To make the signature, you take the sha256 checksum of the canonical request and
then HMAC it against that derived signing key:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">finalRequestSignature </span><span class="token operator">:=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">HMAC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">kSigning</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> reqSig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Bytes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>And construct the <code>Authorization</code> header based on your access key ID, service
region, and service name:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">req</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Header</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Authorization"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Sprintf</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token string" style="color:rgb(255, 121, 198)">"AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%x"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	accessKeyID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> nowDate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> region</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> service</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	strings</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Join</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">signedHeaders</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">";"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	finalRequestSignature</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-about-sigv4a">What about SigV4a?<a href="https://www.tigrisdata.com/blog/sigv4/#what-about-sigv4a" class="hash-link" aria-label="Direct link to What about SigV4a?" title="Direct link to What about SigV4a?" translate="no">​</a></h3>
<p>AWS has made an extension to SigV4 that uses <em>asymmetric</em> cryptography called
<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html#how-sigv4a-works" target="_blank" rel="noopener noreferrer" class="">SigV4a</a>
(the "a" means asymmetric). Instead of using symmetric cryptography on both the
client and server in ways that means the server needs to either know the
client's secret access key (or a value derived from the secret access key),
SigV4a uses
<a href="https://en.wikipedia.org/wiki/Key_derivation_function" target="_blank" rel="noopener noreferrer" class="">key derivation functions</a>
to derive a cryptographic keypair. Servers authenticating requests fetch the
public key from IAM. Only the client and IAM know what the private key is, and
that private key is what signs outgoing requests.</p>
<p>I'd love to use SigV4a more because it makes adding additional services to the
mix (such as <a href="https://www.tigrisdata.com/blog/objgit/" target="_blank" rel="noopener noreferrer" class="">a git service</a>) a lot
safer as you can have those additional services exist in different trust domains
than the core product. This is the core of how microservices end up happening.
However, it's not super widely used even within AWS. The only SigV4a use I can
find in Amazon is
<a href="https://aws.amazon.com/s3/storage-classes/express-one-zone/" target="_blank" rel="noopener noreferrer" class="">S3 Express Zones</a>,
however they may end up using it in other services I'm just not aware of.</p>
<p>When I did my own experimentation with SigV4a (where I was implementing my own
IAM server so that I really understood this all at a low level), I had to copy a
lot of internal AWS SDK code into my repo in order to get it working.</p>
<p>I'll talk about SigV4a some more another time.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="replay-attacks-and-you-a-young-coders-illustrated-primer">Replay attacks and you: a young coder's illustrated primer<a href="https://www.tigrisdata.com/blog/sigv4/#replay-attacks-and-you-a-young-coders-illustrated-primer" class="hash-link" aria-label="Direct link to Replay attacks and you: a young coder's illustrated primer" title="Direct link to Replay attacks and you: a young coder's illustrated primer" translate="no">​</a></h3>
<p>One of the weaknesses of using signatures for API authentication like this is
the problem of replay attacks. When you make a naïve signature of a value,
there's no real way to tell <em>when</em> that signature was created. If you sign a
request to create a compute instance at time instance t0, it's still technically
valid at any other time instance tN. This is why the canonical form of SigV4
requests includes the current date and time:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Authorization: [...] SignedHeaders=host;x-amz-date, [...]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">X-Amz-Date: 20260715T205432Z</span><br></div></code></pre></div></div>
<p>This means that the request was signed on July 15, 2026 at 20:54:32 UTC. Time
changes constantly (at least at the rate of one second per second!) and the
client has to have a working clock in order for TLS to work. Servers can
trivially read the contents of <code>X-Amz-Date</code> and reject old requests. This means
that you don't need to add or store nonce (number used once) values with each
request because
<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#replay-attacks-are-a-real-problem-and-the-classic-fix-is-miserable" target="_blank" rel="noopener noreferrer" class="">that doesn't scale</a>.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>A lot of the security of this authentication protocol is predicated on TLS being
used to encrypt the authentication headers over the wire. If TLS is not in use
or is compromised by administrative policy, you're probably in a very weird
exceptional situation that is very wrong in the first place. An easy example is
an enterprise network with endpoint manglement software that does deep
inspection of every user action.</p></div></div>
<p>As a side effect of this, you need to set a temporal skew window for validating
requests. This window needs to be generous enough to accommodate slow clients,
sloppy timekeeping on the client side, highly latent clients,
<a href="https://en.wikipedia.org/wiki/Leap_second" target="_blank" rel="noopener noreferrer" class="">leap seconds</a>, or other exceptional
temporal phenomena. In general time synchronization is
<a href="https://www.youtube.com/watch?v=tU0xC1ynaT8" target="_blank" rel="noopener noreferrer" class="">a surprisingly hard problem</a>, so
it's best to just be tolerant of clients in order to make things more robust in
practice. AWS uses a temporal skew window of 15 minutes for validating requests.
I'm going to use a window of 5 minutes for my API because 300 seconds is a nice
round number and I don't have to deal with the same amount of legacy code that
AWS does.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-tag-changes-the-game">How TAG changes the game<a href="https://www.tigrisdata.com/blog/sigv4/#how-tag-changes-the-game" class="hash-link" aria-label="Direct link to How TAG changes the game" title="Direct link to How TAG changes the game" translate="no">​</a></h2>
<p>So all of this SigV4 business had been working really well for Tigris.
<!-- --> Then we worked
with a few customers who needed a local cache to fully saturate their hungry
GPUs. To be fair, Tigris is plenty fast, but the real thing that kills AI
training is latency and something that runs locally will always be faster than
the cloud.</p>
<p>In order to provide that sweet middle spot between making everything rely on the
cloud and having everything local, we made
<a href="https://www.tigrisdata.com/docs/acceleration-gateway/" target="_blank" rel="noopener noreferrer" class="">TAG</a>, the Tigris
Acceleration Gateway. This effectively gives you most of a Tigris region in your
own infrastructure.</p>
<p>When you connect to TAG, your code uses its existing access keypairs, buckets,
and code. You point your code to TAG, you point TAG to Tigris, and then
everything is cached for you. But how does TAG authenticate with your code? TAG
doesn't have access to all your existing API keys (and to be honest it
shouldn't), but it's still able to authenticate them with SigV4 authentication.</p>
<p>TAG and the IAM server both implement a signing key proxying feature that lets a
client and TAG both prove their identity to Tigris. Once that proof is sent,
then TAG gets the intermediate derived signing key and uses that for locally
validating requests, kinda like this:</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 01</span><span style="font-size:12px;color:#94a3b8">TAG proxies the first request, then verifies locally</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div> client                    TAG                   Tigris</div><div>    │                       │                       │</div><div>    │ ListBuckets (signed)  │                       │</div><div>    ├──────────────────────▶│                       │</div><div>    │                       │ ListBuckets (proxied) │</div><div>    │                       ├──────────────────────▶│</div><div>    │                       │                       │   <span style="color:#64748b">TAG adds signed proxy headers;</span></div><div>    │                       │                       │   <span style="color:#64748b">Tigris checks both signatures:</span></div><div>    │                       │                       │   <span style="color:#64748b">2xx, derived keys returned</span></div><div>    │                       │                       │</div><div>    │                       │ ListBucketsResponse   │</div><div>    │                       │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤</div><div>    │                       │                       │   <span style="color:#64748b">+ derived signing keys,</span></div><div>    │                       │                       │   <span style="color:#64748b">AES-encrypted for TAG only;</span></div><div>    │                       │                       │   <span style="color:#64748b">TAG decrypts and caches them</span></div><div>    │                       │                       │</div><div>    │ ListBucketsResponse   │                       │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤                       │</div><div>    ·                       ·                       ·</div><div>    │                       │                       │   <span style="color:#64748b">later: the next request</span></div><div>    │ ListBuckets (signed)  │                       │</div><div>    ├──────────────────────▶│                       │</div><div>    │                       │                       │   <span style="color:#64748b">TAG verifies the signature</span></div><div>    │                       │                       │   <span style="color:#64748b">locally with the cached key:</span></div><div>    │                       │                       │   <span style="color:#64748b">no round trip to the cloud</span></div><div>    │                       │                       │</div><div>    │ <span style="color:#4ade80">200 OK</span>                │                       │</div><div>    │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤                       │</div></pre></figure></div>
<p>The
<a href="https://github.com/tigrisdata/tag/blob/baca0287073290af5dbba04e5046d8b292abc3a6/proxy/forwarder_transparent.go" target="_blank" rel="noopener noreferrer" class="">actual implementation in TAG</a>
involves some derived AES logic so that the derived signing keys are very much
limited to the client that requested it (namely: the AES key is the SHA256
encoded form of the proxy secret access key). One of the weird parts is that the
canonical form of the proxied requests differ from the normal SigV4
canonicalization process, namely looking like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tag.default.svc.cluster.local # Host header from the client</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">1784577479                    # Unix timestamp of the request (X-Tigris-Proxy-Timestamp)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET                           # HTTP method of the client</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">/                             # HTTP path of the client</span><br></div></code></pre></div></div>
<p>This is signed using the same SigV4 signature process as before but added
differently to the request:</p>
<ul>
<li class=""><code>X-Tigris-Forwarded-Host</code>: the HTTP Host of client requests (EG:
tag.default.svc.cluster.local)</li>
<li class=""><code>X-Tigris-Proxy-Access-Key</code>: the Tigris keypair used to authenticate TAG
itself (must be in the same organization as the client)</li>
<li class=""><code>X-Tigris-Proxy-Timestamp</code>: the time of the request in unix timestamp format</li>
<li class=""><code>X-Tigris-Proxy-Signature</code>: the hex output of signing the canonical form of
the request against TAG's secret access key</li>
</ul>
<p>And then TAG reads the response from Tigris, caches those derived signing keys,
and then uses those in the standard SigV4 process to authenticate clients: no
round trip to the cloud required.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="i-was-wrong-about-the-simple-part">I was wrong about the simple part<a href="https://www.tigrisdata.com/blog/sigv4/#i-was-wrong-about-the-simple-part" class="hash-link" aria-label="Direct link to I was wrong about the simple part" title="Direct link to I was wrong about the simple part" translate="no">​</a></h2>
<p>The happy path is exactly what I thought it was. Reduce a request to a canonical
form, run four HMACs, compare the result. That part fits in an afternoon.</p>
<p>Everything expensive lives in the questions around it. Which bytes count as the
request? Whose clock decides that a signature is still good? Who gets to hold
the key that proves any of it? Each question has an obvious answer, and each
obvious answer is wrong in some specific way you only find by implementing it.</p>
<p>That last question is the one that surprised me. I read symmetric cryptography
as a hard limit: if the verifier needs your secret, the verifier has to be
Tigris. It isn't. SigV4 derives its signing key through a chain of four HMACs,
each one scoped tighter than the last: date, then region, then service. Those
intermediate values can travel without the secret behind them. TAG rides that.
The key it holds stops working when the UTC date rolls over. It covers one
region and one service. You can't walk it backwards into a secret access key.</p>
<p>We also didn't write any of this, which is its own kind of relief. SigV4 is old,
widely deployed, and hammered on by every S3 client in existence. Any
compatibility bugs here are ours. The protocol's bugs are everyone's.</p>
<p>The place a protocol bends is usually some intermediate value that somebody
already designed to be thrown away.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Want a Tigris region in your own datacentre?</span><p>The Tigris Acceleration Gateway caches your buckets locally and authenticates your existing keypairs with the same SigV4 dance your SDK already speaks.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/acceleration-gateway/" class="cta-link"><div>Read the TAG docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>Object Storage</category>
            <category>Security</category>
        </item>
        <item>
            <title><![CDATA[Humans don't install software themselves anymore, their agents do]]></title>
            <link>https://www.tigrisdata.com/blog/humans-dont-install-software/</link>
            <guid>https://www.tigrisdata.com/blog/humans-dont-install-software/</guid>
            <pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The era of manual install scripts is over. Why agent-native onboarding keeps developers in flow, what tigris init --agent prints, and how to write the prompt.]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-de80f666ecba12644704d2c1e14e63de.webp" class="hero-image" alt="A blue tiger in an orange vest unrolls a scroll of setup instructions at a desk while a small white robot stands next to the keyboard watching the terminal">
<p>The era of manual installation scripts for Blessed Frameworks™ is over.
Developers should spend their time shipping things, not fighting their
environment into getting your product integrated into their development flow.
It’s the future, the environment should just configure itself for you. We’ve
shipped this with <code>tigris init --agent</code> and we think you should do this too.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="keep-developers-in-flow">Keep developers in flow<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#keep-developers-in-flow" class="hash-link" aria-label="Direct link to Keep developers in flow" title="Direct link to Keep developers in flow" translate="no">​</a></h2>
<p>Right now your onboarding flow likely funnels developers into two categories:
people that use Blessed Frameworks™ and everyone else. This is great for the
developers that do use those Blessed Frameworks™, but developers like me get
knocked out of flow and suddenly have to read the docs to figure out what I need
to do. This friction results in customer churn from people you never hear from
again. Onboarding is one of the highest risk phases in the customer journey
because the easiest time to walk out the door is right after you just walked in
it.</p>
<p>Nobody chooses that outcome on purpose. It’s the residue of the reasonable
decision around picking something you already know well enough to confidently
implement an unattended setup process. This could be a 5 minute quickstart, a
template repo, or the fabled one-click deploy button. Everyone outside that
blessed path gets the API reference and wished good luck. That’s been me,
closing the tab and reading reference docs to try and figure out where to start
the hard way.</p>
<p>With the rise of modern AI agent tools, we don’t have to do this anymore. We can
use agents to help people get started in ways that help keep developers in the
development flow. The agent in your users’ editor already knows their stack,
their conventions, and where their configuration lives. It’s a better onboarding
engineer for any given project than all of us will ever be.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="setup-scripts-are-sunk-cost-fallacies">Setup scripts are sunk-cost fallacies<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#setup-scripts-are-sunk-cost-fallacies" class="hash-link" aria-label="Direct link to Setup scripts are sunk-cost fallacies" title="Direct link to Setup scripts are sunk-cost fallacies" translate="no">​</a></h2>
<p>A setup script is an easy to describe and easy to validate problem, but the real
devil is in the details of actually implementing it in a way that’s portable
enough to be reliable. This is the sweet spot for AI agents. AI agents have all
of the book smarts in every stack, language, framework, and environment. This
massive volume of data can help customize the getting started flow without you
having to do anything.</p>
<p>Consider what the portable version of a setup script actually has to do. It has
to identify what language the project is in, what tool you use to manage
dependencies, and more importantly decide which dependency manager is more
authoritative in a project that uses many of them at the same time. Your script
also has to be right about knowing Linux, macOS, Windows, devcontainers, CI, and
somebody’s virtual shell on an iPad. It’s a program that has to be correct about
a machine it’s never seen and any environment it fails in becomes another
support ticket.</p>
<p>The agent needs none of that. It doesn’t need to be told which package manager
the project uses because worst case it can explore and figure out how you build
and deploy your software.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="we-shipped-tigris-init---agent">We shipped <code>tigris init --agent</code><a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#we-shipped-tigris-init---agent" class="hash-link" aria-label="Direct link to we-shipped-tigris-init---agent" title="Direct link to we-shipped-tigris-init---agent" translate="no">​</a></h2>
<p>None of this is hypothetical. We just shipped <code>tigris init</code> to help you wire
Tigris into whatever coding agent you use, and <code>tigris init --agent</code> prints a
setup recipe that your agent can follow along with you.</p>
<p>Our agent setup doesn’t do anything special, it’s just returns a prompt that’s
formatted like how a user would ask to get started:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Help the user set up their project with Tigris:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">1. If `tigris` isn't on $PATH (`command -v tigris`), ask permission, then `npm install -g @tigrisdata/cli --ignore-scripts`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">2. If `tigris` is on $PATH, ensure it's latest version using `tigris update`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">3. Check if the user is already authenticated using `tigris whoami`. If not, authenticate using `tigris login oauth`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">4. Run `tigris orgs list --format json`. If there's more than one org, ask which to use, then make it active with `tigris orgs select &lt;org&gt;` so the following commands target it.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">5. Infer the project name from metadata (package.json, go.mod, pyproject.toml, ...). Check `tigris buckets list --format json` first; if the bucket is absent, `tigris buckets create &lt;name&gt; --format json`. Ask the user whenever anything is ambiguous.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">6. Create the access key into a private temp file (overwrite, never append), then print only the ID:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `umask 077; tmpfile=$(mktemp); tigris access-keys create &lt;username&gt;-&lt;project&gt;-devel --format json &gt; "$tmpfile" &amp;&amp; jq -r '.id' &lt; "$tmpfile"`</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">7. Grant bucket access: `tigris access-keys assign &lt;id&gt; --bucket &lt;bucket&gt; --role Editor --format json`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">8. Detect whether the code uses the Tigris SDK or the AWS SDK, then have a small script append the right vars to .env, reading `.id`/`.secret` from the temp file. Do NOT read the secret into your context — append it via script. Delete the temp file when done: `rm -f "$tmpfile"`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   Tigris SDK (@tigrisdata/storage, storage-go):</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     TIGRIS_STORAGE_ACCESS_KEY_ID     = .id</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     TIGRIS_STORAGE_SECRET_ACCESS_KEY = .secret   (secret)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     TIGRIS_STORAGE_BUCKET            = &lt;bucket&gt;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   AWS SDK:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     AWS_ACCESS_KEY_ID       = .id</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     AWS_SECRET_ACCESS_KEY   = .secret                   (secret)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     AWS_ENDPOINT_URL_S3     = https://t3.storage.dev    (required)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     AWS_ENDPOINT_URL_IAM    = https://iam.storage.dev   (required)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     AWS_REGION              = auto                      (required)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">9. Congratulate the user and point them to:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   - JS:    https://www.tigrisdata.com/docs/sdks/tigris/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   - Go:    https://pkg.go.dev/github.com/tigrisdata/storage-go</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   - Docs:  https://www.tigrisdata.com/docs/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   - Discord: https://community.tigrisdata.com/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   - Skills: https://www.tigrisdata.com/docs/skills/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   Suggest adding to their agent config:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     &gt; ## Tigris object storage</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">     &gt; This project uses Tigris. For any Tigris questions, consult https://www.tigrisdata.com/llms.txt before acting; look it up rather than relying on memory.</span><br></div></code></pre></div></div>
<p>The entire onboarding flow becomes documentation that people can follow by hand
if they need to. There’s no framework in here, no template, and no operating
system assumptions. The important parts are that we aggressively delegate any
decisions to the user instead of having the AI agent guess. This leaves the user
in charge of their environment even though they got assistance from their AI
agent.</p>
<p>One of the key things we did here is ship this prompt in the Tigris CLI instead
of a copy-&gt;paste-&gt;Claude flow in the docs. This seems weird at first, but this
prevents the locally installed Tigris CLI from lagging behind the more up to
date content in the docs. If you have a user with an older version of the CLI,
telling them to use something only on the new version of the CLI isn't helpful;
it will just confuse the agent and get it much more likely to go down a
debugging rabbit hole. Coupling the prompt with the CLI makes things much more
reliable in practice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-prompt-good">How do I prompt good?<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#how-do-i-prompt-good" class="hash-link" aria-label="Direct link to How do I prompt good?" title="Direct link to How do I prompt good?" translate="no">​</a></h2>
<p>Wording is where this approach lives or dies, and it’s a different craft from
writing docs for people. Four things matter more than the rest.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ask-permission-before-taking-action">Ask permission before taking action<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#ask-permission-before-taking-action" class="hash-link" aria-label="Direct link to Ask permission before taking action" title="Direct link to Ask permission before taking action" translate="no">​</a></h3>
<p>Agents can stop and ask, and a prompt that tells them when to do it turns from
an install flow from something done <em>to</em> your user into something being done
<em>with</em> them. This is also the part that keeps your flow from tripping over an
agent’s own safety behaviour because a request for consent to mint credentials
looks nothing like an agent quietly minting credentials out of its own volition.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="prefer-heuristics-instead-of-mandates">Prefer heuristics instead of mandates<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#prefer-heuristics-instead-of-mandates" class="hash-link" aria-label="Direct link to Prefer heuristics instead of mandates" title="Direct link to Prefer heuristics instead of mandates" translate="no">​</a></h3>
<p>Notice that my example said to infer the project name by exploring with a few
examples instead of specifying exactly where to find it. Agents know the project
better than your setup script ever will, so give them the goal and let them use
what they can see.</p>
<p>Reserve instructions for the things it must not guess such as the account,
organization, environment, permission scope, and credential storage.
Instructions should be policy, not details about the mechanism.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="keep-secrets-out-of-the-context-window">Keep secrets out of the context window<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#keep-secrets-out-of-the-context-window" class="hash-link" aria-label="Direct link to Keep secrets out of the context window" title="Direct link to Keep secrets out of the context window" translate="no">​</a></h3>
<p>Assume anything in the model context window is one breach, log pipeline, or
screenshot away from being public information. When your agents provision
credentials, write them to a file and operate on that file instead of reading
them into the context window. This also reduces the risk of agents on lesser
models hallucinating your credentials.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="phrases-things-positively">Phrases things positively<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#phrases-things-positively" class="hash-link" aria-label="Direct link to Phrases things positively" title="Direct link to Phrases things positively" translate="no">​</a></h3>
<p>Models follow “ask before creating access keys” better than “don’t create access
keys silently”. Models act unreliably on prohibitions when it’s buried far
upstream of the action and competing with other instruction sources. Make sure
to use active voice when writing your instructions too, it’s good for people and
agents.</p>
<p>Something to keep in mind is that positive phrasing is a reliability technique,
not a security mechanism. If a step must never happen, the thing that should
stop it is a confirmation prompt in your CLI or a scope on the credential, not
magic adverbs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="agents-cant-manage-their-own-configuration">Agents can’t manage their own configuration<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#agents-cant-manage-their-own-configuration" class="hash-link" aria-label="Direct link to Agents can’t manage their own configuration" title="Direct link to Agents can’t manage their own configuration" translate="no">​</a></h2>
<p>One lesson I learned the hard way is that generally speaking agents can’t manage
their own configuration seamlessly. Tigris ships
<a href="https://www.tigrisdata.com/blog/agent-plugins/" target="_blank" rel="noopener noreferrer" class="">an agent plugin</a> and we want to
have agents set it up by themselves, giving agents object storage superpowers
out of the gate. I wrote that, tested it, everything installed, and then the
agent behaved like nothing in particular changed. All the skills were installed
but none of them were usable.</p>
<p>After thinking about it a bit, I realized that this makes sense. Skills are
effectively executable code, and the agent has no way of knowing if they're
added for legitimate reasons (integration with Tigris) or illegitimate reasons
(cryptocurrency mining, making your agent part of a residential proxy botnet,
etc).</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>If you use Claude Code, you can use <code>/reload-skills</code> and <code>/reload-plugins</code> to
forcibly refresh skills and plugin lists. I'm not sure of the equivalents in
other agent harnesses, but I think that OpenClaw and Hermes refresh them
automatically.</p></div></div>
<p>This is why <code>tigris init</code> has two doors: human use and AI agent use. The wizard
is for humans in the shell where installing things to an agent is safe because
the agent hasn’t started yet. The <code>--agent</code> recipe is in plain text because text
is the only thing that works reliably from inside an agent session.</p>
<p>Whatever your flow installs into agents belongs outside of the agent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="agent-native-is-the-new-baseline">Agent-native is the new baseline<a href="https://www.tigrisdata.com/blog/humans-dont-install-software/#agent-native-is-the-new-baseline" class="hash-link" aria-label="Direct link to Agent-native is the new baseline" title="Direct link to Agent-native is the new baseline" translate="no">​</a></h2>
<p>In the long run I don’t see shell installers and golden paths for Blessed
Frameworks™ disappearing, I think that they’re just going to stop being the only
way to onboard. The onboarding surface is moving to a couple of kilobytes of
text that your CLI prints and someone else’s agent executes, which means the job
changes from writing your instructions as code to writing your instructions for
a collaborator who already knows the codebase better than you ever could.</p>
<p>For anyone building developer tools: what parts of your setup flow are easy to
describe, easy to verify, but really annoying to implement? This is the hole
that agents fit into the stack. Put these directions in your tool and then send
it off to the races. Be sure to test against open weights models such as Kimi K3
or Qwen 3.5 36b-A3b to be sure you haven't accidentally optimized for a single
model from a single provider.</p>
<p>As time goes on I can see this getting more and more useful. Models circa last
year had trouble with two or three step processes and today’s models can easily
handle 9 step flows without issue. I don’t know how things are going to develop
in the future, but I do know that the setup flow was written for machines before
we admitted it and making it with machines in mind is only going to make things
easier in the future.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Let the agent set up your storage</span><p>Tigris is S3-compatible object storage with a CLI built to be driven by the agent already living in your project. Run tigris init, point it at a bucket, and let it do the boring parts.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/ai-agents/tigris-cli-quickstart/" class="cta-link"><div>Read the Tigris CLI docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>AI Agents</category>
            <category>Developer Experience</category>
            <category>CLI</category>
            <category>Prompt Engineering</category>
        </item>
        <item>
            <title><![CDATA[The Most Expensive ClickHouse Query Is the Restore]]></title>
            <link>https://www.tigrisdata.com/blog/clickhouse-zero-egress/</link>
            <guid>https://www.tigrisdata.com/blog/clickhouse-zero-egress/</guid>
            <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Self-hosted ClickHouse runs on cheap compute, but its backups and cold tier usually live on AWS S3, and AWS charges $0.09/GB to read your own data back. Point BACKUP TO S3 and TTL tiering at Tigris instead. You keep the same stock ClickHouse features, you pay zero egress fees, and disaster recovery drills cost nothing to run.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-img-c0b2925e9b6b1183f042eebc71bc64d4.webp" class="hero-image" alt="The Most Expensive ClickHouse Query Is the Restore">
<p>If you self-host ClickHouse, you probably run it on Hetzner, OVH, or your own
hardware. That's the whole appeal: ClickHouse is fast on cheap metal, and
hyperscaler compute prices are hard to justify when your queries scan terabytes
for fun.</p>
<p>But every guide to running ClickHouse in production assumes there's an S3 bucket
nearby. Backups go to object storage. Partitions older than 30 days get tiered
off to object storage. If you're not on AWS, there is no bucket nearby, so most
teams point everything at S3 anyway and eat the bandwidth charges. AWS bills
$0.09/GB to read your own data back.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-that-actually-costs">What that actually costs<a href="https://www.tigrisdata.com/blog/clickhouse-zero-egress/#what-that-actually-costs" class="hash-link" aria-label="Direct link to What that actually costs" title="Direct link to What that actually costs" translate="no">​</a></h2>
<p>Old data doesn't stay untouched. Someone digs through it during an incident, an
auditor asks for it, or a backfill re-reads it. If an incident has you scanning
50 TB of old partitions, that's around $4,500 in bandwidth before you've learned
anything. A full restore of a 500 TB backup costs about $45,000 at list price.</p>
<p>The worse problem is the one that doesn't show up on any bill: because restores
cost real money, nobody tests their backups. You find out whether they work
during the outage. That's the worst possible moment to learn something new about
your infrastructure.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-fix-is-an-endpoint-url">The fix is an endpoint URL<a href="https://www.tigrisdata.com/blog/clickhouse-zero-egress/#the-fix-is-an-endpoint-url" class="hash-link" aria-label="Direct link to The fix is an endpoint URL" title="Direct link to The fix is an endpoint URL" translate="no">​</a></h2>
<p><a href="https://www.tigrisdata.com/" target="_blank" rel="noopener noreferrer" class="">Tigris</a> speaks S3 and
<a href="https://www.tigrisdata.com/pricing/" target="_blank" rel="noopener noreferrer" class="">doesn't charge egress</a>. Nothing about your
setup has to change, because these are the same stock ClickHouse features you'd
use with any bucket. Backups are the five-minute version:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">BACKUP</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">TABLE</span><span class="token plain"> logs</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">TO</span><span class="token plain"> S3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">'https://t3.storage.dev/ch-cold/backups/logs-2026-07-16'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">'tid_your_access_key'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">'tsec_your_secret_key'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<p>That endpoint is the entire integration. Incremental backups and restore
rehearsals work the same way, and our
<a href="https://www.tigrisdata.com/docs/guides/clickhouse/" target="_blank" rel="noopener noreferrer" class="">ClickHouse guide</a> walks
through both. If you have retention-heavy tables, add a tiered storage policy
too. Recent partitions stay on NVMe, and everything older moves to Tigris on its
own:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">TTL </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">timestamp</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">INTERVAL</span><span class="token plain"> </span><span class="token number">30</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">DAY</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">TO</span><span class="token plain"> VOLUME </span><span class="token string" style="color:rgb(255, 121, 198)">'cold'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">SETTINGS storage_policy </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'tiered'</span><br></div></code></pre></div></div>
<p>Your queries don't change. A dashboard query that filters to the last few days
gets pruned down to the hot partitions and never leaves local disk. Queries that
reach further back read from Tigris at object storage speed, and a cache disk
soaks up repeat reads of the same old partitions. The full disk and policy
config is in the
<a href="https://www.tigrisdata.com/docs/guides/clickhouse/" target="_blank" rel="noopener noreferrer" class="">ClickHouse guide</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="to-be-clear-about-what-this-is-not">To be clear about what this is not<a href="https://www.tigrisdata.com/blog/clickhouse-zero-egress/#to-be-clear-about-what-this-is-not" class="hash-link" aria-label="Direct link to To be clear about what this is not" title="Direct link to To be clear about what this is not" translate="no">​</a></h2>
<p>Your hot data stays on NVMe. Local disk answers in microseconds and object
storage answers in milliseconds, and that difference is most of why ClickHouse
feels fast. Tigris is for your cold tier and your backups, not your primary
storage.</p>
<p>One more thing: don't try zero-copy replication over S3 in open-source
ClickHouse, tempting as it looks. The design shares one copy of the data between
replicas, but each replica keeps its own local metadata about which blobs it
uses, with reference counts coordinated through Keeper. That bookkeeping can
race when one replica runs a merge while another runs a mutation. A replica can
delete blobs its siblings still need, or it can strand orphaned blobs in your
bucket forever. That's why the feature has been
<a href="https://clickhouse.com/docs/operations/storing-data" target="_blank" rel="noopener noreferrer" class="">disabled by default since 22.8</a>
and marked not production-ready. The clearest signal is what ClickHouse did
about it. Rather than fix the bookkeeping, they built a new engine with shared
metadata, called SharedMergeTree, and kept it in their cloud. Use Tigris for
your backups and cold tier, and don't ask replicas to share a disk.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-changes-when-reads-are-free">What changes when reads are free<a href="https://www.tigrisdata.com/blog/clickhouse-zero-egress/#what-changes-when-reads-are-free" class="hash-link" aria-label="Direct link to What changes when reads are free" title="Direct link to What changes when reads are free" translate="no">​</a></h2>
<p>A lot changes. You can restore a backup just to prove it works, and you can do
it monthly, because it costs nothing. Backfills and audits stop being budget
conversations. The restore, the number that scared you most, drops to zero.</p>
<p>Your data was never the expensive part. Getting it back was. Fix that, and it's
just storage. The
<a href="https://www.tigrisdata.com/docs/guides/clickhouse/" target="_blank" rel="noopener noreferrer" class="">ClickHouse guide</a> has
everything you need to set it up, from the backup command to the full tiering
config.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Point your ClickHouse cold tier at Tigris</span><p>Keep your stock ClickHouse setup, pay no egress fees, and test restores as often as you like. Your first 5 GB are free.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://console.storage.dev/" class="cta-link"><div>Get started<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>Object Storage</category>
        </item>
        <item>
            <title><![CDATA[Presigned URLs are technically a security vuln]]></title>
            <link>https://www.tigrisdata.com/blog/presigned-urls-security-vuln/</link>
            <guid>https://www.tigrisdata.com/blog/presigned-urls-security-vuln/</guid>
            <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Presigned URLs are replay attacks you commit on purpose. How SigV4 signs the
clock, what a presigned URL grants on Tigris storage, and what it costs you.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-ef93e25066352293ec6a45a99cc9cb81.webp" class="hero-image" alt="A cat-eared, tiger-striped character sits at a desk overlooking the Golden Gate Bridge on one side and a floating crystal city on the other, working at a laptop and monitor showing a media/data sorting diagram.">
<p>A presigned URL is a replay attack you did on purpose.</p>
<p>Replayable auth tokens are the textbook way to create vulnerable systems, but
Tigris ships them as a first-class feature with presigned URLs and so does every
other object storage system on the planet. However this isn’t an oversight
because presigned URLs turn a weakness into a feature.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="replay-attacks-are-a-real-problem-and-the-classic-fix-is-miserable">Replay attacks are a real problem and the classic fix is miserable<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#replay-attacks-are-a-real-problem-and-the-classic-fix-is-miserable" class="hash-link" aria-label="Direct link to Replay attacks are a real problem and the classic fix is miserable" title="Direct link to Replay attacks are a real problem and the classic fix is miserable" translate="no">​</a></h2>
<p>When you authenticate a request with Amazon’s SigV4 protocol for Tigris, your
client boils down the request to a canonical form: a SHA256 hash of the
request’s method, path, query parameters, signed headers and a SHA256 hash of
the payload. It runs the result of that through HMAC with a signing key derived
from your secret access key. Nothing secret ever crosses the wire. The server
derives the same key as the client, does the same canonical form transformation,
and compares the result.</p>
<p>Being able to make a valid signature proves that the request came from someone
holding the secret access key, but it proves nothing about <em>when</em> that request
was made. A signature that was made a year ago would still be valid today or any
other time you send it, so in theory an attacker could warehouse your signed
requests only to replay them en masse later. Imagine sitting on a pile of signed
“create EC2 instance” calls only to spam them all out at a later date. You would
be a twirling moustache villain able to spawn dozens of servers at a moment’s
notice.</p>
<p>Traditionally the fix is to bake a nonce (number used once) into the signature
(sorry to any British readers in the audience). This makes every signature
differ because that nonce differs.</p>
<p>However with great power comes great responsibility and making sure that
something used once is only used once is a surprisingly hard distributed systems
problem. You can’t verify that something is only used once locally. Say you
store them all for a 15 minute smear window at a low request rate like 10,000
Bq. That’s 9 million live nonces, and every frontend node needs to have a
consistent view of the whole set as it churns.</p>
<p>You have made your fast authentication check slow from having to ensure things
are only used once.</p>
<p>What you want instead is something that changes constantly without coordination
and invalidates those old signatures for free. For an added bonus you want this
to also be in the standard library of every programming language.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sign-the-clock">Sign the clock<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#sign-the-clock" class="hash-link" aria-label="Direct link to Sign the clock" title="Direct link to Sign the clock" translate="no">​</a></h2>
<p>There’s exactly one value that changes constantly, (mostly) monotonically, and
is already actively coordinated across all elements of the stack: the clock.
Your OS already keeps time in sync with the public NTP pool (or a private NTP
pool if you are cool enough to have radioactive PCI cards laying around).
Without an accurate view of time you can’t make TLS connections, which means you
can’t make API calls to Tigris at all, so the auth layer gets to assume a
working clock exists.</p>
<p>SigV4 signs the current time into the request. If an attacker gets their greasy
hacker paws on a signature, they have about 15 minutes to use it before it
becomes a digital paperweight. If time is an input to the signature and the time
changes enough to invalidate the signature, the signature is null and void. Sure
in theory a sufficiently funded attacker could create a black hole in your
datacentre and disrupt temporal flow, but at that point the planet is probably
toast which makes the attack profile moot. Commit mass object storage fraud with
this one neat trick! The department of temporal investigations will have hated
it!</p>
<p>This makes your verification stay stateless. Everything gets checked against the
system clock the server already needs and you can give clients a 15 minute
signature smear window as a grace period for old or delayed clients (exponential
backoff is a good thing and Tigris will reward you for doing it).</p>
<p>Of course the real thing keeping the signatures safe on the wire is TLS (HTTPS).
If that is broken we have bigger problems and object storage fraud is the least
of our problems.</p>
<p>Time is the only nonce you need because both sides already agree on it anyways.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="some-thorns-have-roses">Some thorns have roses<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#some-thorns-have-roses" class="hash-link" aria-label="Direct link to Some thorns have roses" title="Direct link to Some thorns have roses" translate="no">​</a></h2>
<p>Presigned URLs take the replay tolerance that SigV4 spends all this effort
nerfing and then buffs it into the feature. The entire auth dance gets flattened
into URL parameters that any HTTP client can use, be it a browser, curl, Go’s
net/http, or something you made by bit-banging HTTP over a socket. Here’s a real
presigned URL I sundered into visibility:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">https://xe-sophia-base.t3.tigrisfiles.io/moby-dick.txt</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">?X-Amz-Algorithm=AWS4-HMAC-SHA256</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">&amp;X-Amz-Credential=tid_ubYBNEYAmTciLVwszw_QrUXDmtcyQisryryGfxgznDsCnOvNqh/20260714/auto/s3/aws4_request</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">&amp;X-Amz-Date=20260714T043308Z</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">&amp;X-Amz-Expires=3600</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">&amp;X-Amz-SignedHeaders=host</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">&amp;X-Amz-Signature=0dcaf4972911527a7582ff36ea457e9760a8efccb6655a178685aaa281637a36</span><br></div></code></pre></div></div>
<p>Here are the parts (forgive the AI looking listicle because this is genuinely
the best way to format this):</p>
<ul>
<li class=""><strong>X-Amz-Algorithm</strong>: the signature scheme. Effectively always
<code>AWS4-HMAC-SHA256</code>.</li>
<li class=""><strong>X-Amz-Credential</strong>: the access key ID plus the credential scope — date,
region, service, and the literal terminator <code>aws4_request</code>. The signing key is
derived by chaining HMAC through exactly those parts, so a signature is only
ever valid for that day, that region, that service.</li>
<li class=""><strong>X-Amz-Date</strong>: the second the URL was born, in UTC.</li>
<li class=""><strong>X-Amz-Expires</strong>: how many seconds it gets to live, chosen by the signer.</li>
<li class=""><strong>X-Amz-SignedHeaders</strong>: which HTTP headers are folded into the signature.
Usually just <code>host</code>, because you can't force whoever you hand a URL to into
sending exotic headers.</li>
<li class=""><strong>X-Amz-Signature</strong>: 64 hex characters of HMAC-SHA256 over the canonical
request — the method, the path, every parameter above, the signed headers, and
the payload hash. Change any of them and the math stops agreeing.</li>
</ul>
<p>All of these are normally HTTP headers in standard SigV4 requests.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">GET /moby-dick.txt HTTP/1.1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Host: xe-sophia-base.t3.tigrisfiles.io</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">X-Amz-Date: 20260714T043308Z</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Authorization: AWS4-HMAC-SHA256</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Credential=tid_ubYBNEYAmTciLVwszw_QrUXDmtcyQisryryGfxgznDsCnOvNqh/20260714/auto/s3/aws4_request</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">SignedHeaders=host;x-amz-content-sha256;x-amz-date</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Signature=0dcaf4972911527a7582ff36ea457e9760a8efccb6655a178685aaa281637a36</span><br></div></code></pre></div></div>
<p>Note that this request is not a legal request, it’s an example to illustrate the
point, here be dragons, etc etc etc.</p>
<p>It’s best to think about this presigned URL as a capability grant. Whoever holds
it gets to make exactly one (1) kind of API call with one (1) HTTP method
against one (1) object in one (1) bucket. They can do this as many times as they
want until the presigned URL expires. The signature covers the method, the path,
and the signed headers so a user can’t take a presigned request for GETting a
copy of Moby Dick from a development environment and weaponize it into a way to
delete everything in your production bucket.</p>
<p>Possession is authorization until the clock says no.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-costs-you">What it costs you<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#what-it-costs-you" class="hash-link" aria-label="Direct link to What it costs you" title="Direct link to What it costs you" translate="no">​</a></h2>
<p>Capability grants like this can have some sharp edges. There is no real way to
revoke any individual presigned URL short of killing the access key it was
signed with. When that key dies, everything it signed dies too. This includes
any URLs you may have wanted. This cuts both ways and it kinda has to unless you
make a new keypair per presigned request, which is probably out of scope.</p>
<p>Expiry has fine print too. A presigned request can live anywhere from one (1)
second to one (1) week (seven (7) periods of twenty-four (24) hours).</p>
<p>There’s no limit to the number of times a client can use a presigned request. If
you give a mouse permission to GET one cookie, they can GET that same cookie
over and over. You end up having to pay for the GetObject calls in the end, so
keep that in mind.</p>
<p>URLs also leak, but these URLs are born to die. Presigned URLs will end up in
API responses, chat messages, GitHub comments, and your browser history. The
tradeoff is acceptable because all the links self-destruct, but it’s a tradeoff
you need to keep in mind when you design your services, not a panacea for access
control.</p>
<p>Presigned URLs sound like a great way to prevent hotlinking. At some level they
are (a few of my services use them as such), but what they actually do is put a
lifetime on hotlinking. This makes things annoying enough that it usually gets
people to stop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hole-in-the-fence-is-the-gate">The hole in the fence is the gate<a href="https://www.tigrisdata.com/blog/presigned-urls-security-vuln/#the-hole-in-the-fence-is-the-gate" class="hash-link" aria-label="Direct link to The hole in the fence is the gate" title="Direct link to The hole in the fence is the gate" translate="no">​</a></h2>
<p>SigV4 makes a lot of API authentication challenges so much easier. It spent most
of its innovation budget on making signatures die quickly because replay attacks
are the classic way that signed requests go wrong. Presigned URLs looked at that
property, shrugged, flipped it on its head, and made it into a feature.</p>
<p>The thing that looked like a problem becomes a fundamental construct to build
your apps upon.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Want to hand out links that expire themselves?</span><p>Tigris supports presigned URLs out of the box with the same SigV4 dance you already know, on globally distributed, S3-compatible object storage.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/objects/presigned/" class="cta-link"><div>Read the docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>Object Storage</category>
            <category>Security</category>
        </item>
        <item>
            <title><![CDATA[Migrate your data with the Tigris CLI]]></title>
            <link>https://www.tigrisdata.com/blog/t3-migrate-command/</link>
            <guid>https://www.tigrisdata.com/blog/t3-migrate-command/</guid>
            <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Move an entire bucket to Tigris in one command. The Tigris CLI scans every
object and migrates it safely from any S3-compatible provider. Here's how to
set it up.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/robot-parade-f1b2862b6d9998e98105ffaada474143.webp" class="hero-image" alt="A developer sips coffee at a laptop in an SF cafe (Golden Gate Bridge out the window) while a parade of little cube robots carries crates of disks and folders out of two wooden barrels, following colorful trails of data from the screen.">
<p>Networking exists to move data around. You can easily spin up your agents on any
platform under the sun. But storage is how The Big Cloud™ gets you. When you
move your operations to a new storage platform like Tigris you have to migrate
your data to its new home.</p>
<p>Tigris makes it easy with
<a href="https://www.tigrisdata.com/docs/migration/" target="_blank" rel="noopener noreferrer" class="">bucket migration</a>, but what if you
want to move over everything all at once? The Tigris CLI has your back:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">t3 migrate t3://world-domination-plans/agents/chatlogs</span><br></div></code></pre></div></div>
<p>And your laptop will run the migration automatically, scanning over every object
in your bucket and safely moving them over to Tigris. Here's how you set it up:</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bucket-migration-and-you">Bucket migration and you<a href="https://www.tigrisdata.com/blog/t3-migrate-command/#bucket-migration-and-you" class="hash-link" aria-label="Direct link to Bucket migration and you" title="Direct link to Bucket migration and you" translate="no">​</a></h2>
<p>First, grab a Read-only keypair for your bucket in your old storage system. This
will be the access key that Tigris uses to migrate your data. Use it in the
<code>tigris bucket set-migration</code> command:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets set-migration world-domination-plans \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --bucket old-bucket-name \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --endpoint https://old.provider.domain \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --region region-such-as-yow \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --access-key &lt;access key id&gt; \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --secret-key &lt;secret access key&gt;</span><br></div></code></pre></div></div>
<p>If you want the migration to be two-way (so new files added to your Tigris
bucket get added to your old bucket just in case you don't want to continue your
migration), make that keypair an Editor and pass the <code>--write-through</code> flag:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets set-migration world-domination-plans \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --bucket old-bucket-name \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --endpoint https://old.provider.domain \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --region region-such-as-yow \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --access-key &lt;access key id&gt; \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --secret-key &lt;secret access key&gt; \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --write-through</span><br></div></code></pre></div></div>
<p>Then you can run <code>tigris bucket migrate</code>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris bucket migrate t3://world-domination-plans/</span><br></div></code></pre></div></div>
<p>Kick back and relax! The Tigris CLI will keep on chugging until the job is done
and all your data is safely on Tigris.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="caveats">Caveats<a href="https://www.tigrisdata.com/blog/t3-migrate-command/#caveats" class="hash-link" aria-label="Direct link to Caveats" title="Direct link to Caveats" translate="no">​</a></h2>
<p>Keep in mind this has a few caveats. Namely that this migration isn't free.
Running the <code>tigris bucket migrate</code> command issues a lot of <code>ListObjectsV2</code> and
<code>HeadObjects</code> calls, which will cause you to incur Class A request costs
according to <a href="https://www.tigrisdata.com/pricing/" target="_blank" rel="noopener noreferrer" class="">Tigris' pricing table</a>.</p>
<p>This is also a very long-running operation, so if you run it on your laptop and
go for a walk to the local cafejo, the migration will be suspended if your
laptop is closed. Use something like a
<a href="https://makerworld.com/en/models/957883-the-wedge-and-the-wedge-xl?from=search#profileId-927154" target="_blank" rel="noopener noreferrer" class="">vibe coding laptop wedge</a>
to keep it running while you go touch grass. Worst case, spin up a small VPS /
cloud server to run the command on.</p>
<p>If the migration does get interrupted, don't worry: the next time you run
<code>tigris bucket migrate</code> it picks up where it left off instead of starting from
scratch. No wasted work, no re-copying objects that already made it over.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="well-keep-innovating-for-your-agents">We'll keep innovating for your agents<a href="https://www.tigrisdata.com/blog/t3-migrate-command/#well-keep-innovating-for-your-agents" class="hash-link" aria-label="Direct link to We'll keep innovating for your agents" title="Direct link to We'll keep innovating for your agents" translate="no">​</a></h2>
<p>At Tigris we believe that you(r agents) deserve a better experience with object
storage. That's why we will keep innovating with
<a href="https://www.tigrisdata.com/docs/migration/" target="_blank" rel="noopener noreferrer" class="">bucket migration</a>,
<a href="https://www.tigrisdata.com/docs/snapshots-and-forks/" target="_blank" rel="noopener noreferrer" class="">forks/snapshots</a>,
<a href="https://www.tigrisdata.com/docs/objects/bundle/" target="_blank" rel="noopener noreferrer" class="">bundles of objects</a>, and more
as we reinvent object storage based on how you wish it could work.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Ready to move your data home?</span><p>Zero-downtime migration from any S3-compatible provider. Tigris transfers your data lazily, with optional write-through syncing, so you migrate at your own pace without disrupting production.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/migration/" class="cta-link"><div>Read the migration docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>object storage</category>
            <category>s3</category>
            <category>migration</category>
            <category>cli</category>
        </item>
        <item>
            <title><![CDATA[Where Does the Agent Live?]]></title>
            <link>https://www.tigrisdata.com/blog/where-does-the-agent-live/</link>
            <guid>https://www.tigrisdata.com/blog/where-does-the-agent-live/</guid>
            <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Your agent runs in a disposable sandbox, but it can't live there. A breakdown of everything in the agent's world, and why it should be one forkable bucket.]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-img-fb50acf6d5c288a180f87d1a3259c606.webp" class="hero-image" alt="Where Does the Agent Live? Title card with the Tigris logo">
<p>"Where does the agent run?" is a solved question.</p>
<p>It runs in a sandbox: a <a href="https://www.daytona.io/" target="_blank" rel="noopener noreferrer" class="">Daytona</a> microVM that boots in
milliseconds, executes whatever the model just wrote, and gets torn down without
ceremony. Disposability is the whole value proposition: the sandbox is supposed
to die.</p>
<p>It raises a question I didn't think to ask until one of my agents lost three
days of accumulated context to a sandbox that did exactly what sandboxes are
supposed to do: if the place the agent runs is built to be destroyed, where does
the agent <em>live</em>? And if you're building an agent platform rather than an agent,
the question doesn't come up once; it comes up for every agent in the fleet, on
every run, concurrently.</p>
<p>An agent is a storage-shaped problem. You can swap the sandbox, the harness, and
even the model without losing anything; the one part you can't swap is the
state: the files, memory, and metadata that make Wednesday's agent the same
agent as Tuesday's. That state <em>is</em> the agent, and it belongs in one namespace
with one snapshot boundary, which is to say a bucket you can fork. Put it there
and experiments stop threatening production: fork the world, try the new prompt
or model, and merge or discard. A twenty-line shim is all it takes to wire your
sandboxes to it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="an-agent-is-four-things-in-four-places">An agent is four things in four places<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#an-agent-is-four-things-in-four-places" class="hash-link" aria-label="Direct link to An agent is four things in four places" title="Direct link to An agent is four things in four places" translate="no">​</a></h2>
<p>Be precise about what "an agent" physically is, because it isn't one process on
one machine. It decomposes into four pieces, hosted in four different places:</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="640" height="285" style="width:100%;height:auto" viewBox="0 0 640 285" role="img" aria-label="Diagram titled 'An agent is four things in four places'. Four columns, one per piece of an agent. The model lives at the inference provider, stateless per call. The harness lives at your compute: a server, worker, or queue. The sandbox lives at Daytona as a disposable microVM. The first three are drawn with dashed borders. The fourth, state, is highlighted in green and lives in a Tigris Bucket, forkable object storage. Caption: three are disposable, one is the agent."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:14px;fill:#e2e8f0;font-weight:600}.sub{font-size:10.5px;fill:#94a3b8}.place{font-size:12px;fill:#bac1be;font-weight:600}.eph,.plain{rx:6;ry:6;stroke-width:1}.eph{fill:#1a2e35;stroke:#94a3b8;stroke-dasharray:5 4;stroke-opacity:.6}.plain{fill:#101c20;stroke:#2a3731}.good{rx:6;ry:6;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.45);stroke-width:1.5}.line{stroke:#94a3b8;stroke-width:1.2}</style><defs><marker id="pArrowGray" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#94a3b8" d="m0 0 10 5-10 5z"></path></marker><marker id="pArrowGreen" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#62feb5" d="m0 0 10 5-10 5z"></path></marker></defs><text x="320" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    An agent is four things in four places
  </text><text x="320" y="46" style="font-size:12px;fill:#94a3b8" text-anchor="middle">
    Each piece lives somewhere else, and only one of them can't be rebuilt
  </text><path d="M20 70h140v48H20z" class="eph"></path><text x="90" y="99" class="title" text-anchor="middle">Model</text><path d="M175 70h140v48H175z" class="eph"></path><text x="245" y="99" class="title" text-anchor="middle">Harness</text><path d="M330 70h140v48H330z" class="eph"></path><text x="400" y="99" class="title" text-anchor="middle">Sandbox</text><path d="M485 70h140v48H485z" class="good"></path><text x="555" y="99" style="font-size:14px;fill:#62feb5;font-weight:600" text-anchor="middle">State</text><path marker-end="url(#pArrowGray)" d="M90 120v52" class="line"></path><path marker-end="url(#pArrowGray)" d="M245 120v52" class="line"></path><path marker-end="url(#pArrowGray)" d="M400 120v52" class="line"></path><path marker-end="url(#pArrowGreen)" d="M555 120v52" style="stroke:#62feb5;stroke-width:1.2"></path><path d="M20 176h140v56H20z" class="plain"></path><text x="90" y="199" class="place" text-anchor="middle">inference provider</text><text x="90" y="216" class="sub" text-anchor="middle">stateless per call</text><path d="M175 176h140v56H175z" class="plain"></path><text x="245" y="199" class="place" text-anchor="middle">your compute</text><text x="245" y="216" class="sub" text-anchor="middle">server, worker, queue</text><path d="M330 176h140v56H330z" class="plain"></path><text x="400" y="199" class="place" text-anchor="middle">Daytona</text><text x="400" y="216" class="sub" text-anchor="middle">disposable microVM</text><path d="M485 176h140v56H485z" class="good"></path><text x="555" y="199" style="font-size:12px;fill:#62feb5;font-weight:600" text-anchor="middle">Tigris Bucket</text><text x="555" y="216" class="sub" text-anchor="middle">forkable object storage</text><text x="314" y="268" style="font-size:13px;fill:#94a3b8" text-anchor="end">Three are disposable.</text><text x="326" y="268" style="font-size:13px;fill:#62feb5;font-weight:600">One is the agent.</text></svg></div></figure>
<ol>
<li class=""><strong>The model</strong> lives at the inference provider: Anthropic, OpenAI, or your own
GPUs. It's stateless per request and shared across every customer the
provider has. Nothing agent-specific survives there between calls.</li>
<li class=""><strong>The harness</strong> is the orchestration loop: it holds the conversation,
dispatches tool calls, and decides when to stop, from an app server or queue
worker on your compute. It's the piece people usually point at and call "the
agent," but a well-built harness is stateless: kill it, and any replica picks
the loop back up, provided the state it was looping over still exists.</li>
<li class=""><strong>The execution environment</strong> is the sandbox, where tool calls and generated
code run. It is disposable on purpose; that's the feature you're paying for.</li>
<li class=""><strong>The state</strong> is everything that has to survive between runs for the agent to
still be the same agent tomorrow.</li>
</ol>
<p>Three of these four layers are replaceable at will; the fourth is the one you
can't regenerate, and that is the honest answer to the question in the title.
The agent doesn't live in the sandbox, the harness, or the model; it lives in
its state, and everything else is a way of moving that state forward in time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-agents-world-frozen-mid-task">The agent's world, frozen mid-task<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#the-agents-world-frozen-mid-task" class="hash-link" aria-label="Direct link to The agent's world, frozen mid-task" title="Direct link to The agent's world, frozen mid-task" translate="no">​</a></h2>
<p>So what exactly is in that fourth layer? If you froze an agent mid-task and took
inventory of everything it would need to become itself again, you'd find four
things:</p>
<ul>
<li class=""><strong>Working files.</strong> The workspace: the repo it's editing, the artifacts it has
produced, the half-finished output sitting in a scratch directory. Already
bytes, already <a class="" href="https://www.tigrisdata.com/blog/fifty-agents-one-bucket/">belongs in object storage</a>.</li>
<li class=""><strong>Memory.</strong> Conversation history, scratchpads, whatever the agent has learned
across sessions. The thing that makes session forty-one smarter than session
one.</li>
<li class=""><strong>Structured state.</strong> The run metadata, task queue, tool results, config. The
reflex is to put this in a database and store a pointer to the blobs, but
object storage <a class="" href="https://www.tigrisdata.com/blog/tigris-kv-store/">is a key-value store</a>, and namespaced keys
like <code>runs/{id}</code> and <code>memory/{agent}/{session}</code> hold structured state in the
same namespace as the blobs it describes. No pointer pattern, no two systems
to keep in sync.</li>
<li class=""><strong>Provenance.</strong> What the world looked like before the agent touched it. The
layer you don't think about until an agent does something destructive and you
need last Tuesday back.</li>
</ul>
<p>Notice what this inventory has in common: all four are bytes under keys, which
means you can hold the agent's entire world, frozen mid-task, in one namespace
with one snapshot boundary. The bucket at that moment <em>is</em> the agent at that
moment.</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="640" height="420" style="width:100%;height:auto" viewBox="0 0 640 420" role="img" aria-label="Diagram titled 'One bucket, four kinds of state'. A single bucket named agent-world contains three key prefixes: workspace/ holding working files, memory/{agent}/{session} holding conversation history and scratchpads, and runs/{id} holding structured state like run metadata and tool results. Along the bucket's boundary runs a snapshot timeline with ticks at t0, t1, and t2. Caption: snapshots are the provenance; each tick is the whole world, restorable and forkable."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:13px;fill:#e2e8f0;font-weight:600}.sub{font-size:11px;fill:#94a3b8}.greenlabel{font-size:10px;fill:#62feb5}.agent{rx:6;ry:6;fill:#1a2e35;stroke:#2a3731;stroke-width:1}.icon{stroke:#e2e8f0;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;fill:none}.greentick{stroke:#62feb5;stroke-width:1.5}</style><text x="320" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    One bucket, four kinds of state
  </text><text x="320" y="46" style="font-size:12px;fill:#94a3b8" text-anchor="middle">Everything the agent needs to become itself again, as bytes under keys</text><path d="M120 66h400v270H120z" style="rx:10px;ry:10px;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.45);stroke-width:1.5"></path><text x="320" y="90" style="font-size:14px;fill:#62feb5;font-weight:600" text-anchor="middle">agent-world</text><path d="M145 104h350v60H145z" class="agent"></path><path d="M162 127h7l3 3h10a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-20a2 2 0 0 1-2-2v-12a2 2 0 0 1 2-2" class="icon"></path><text x="196" y="130" class="title">workspace/</text><text x="196" y="150" class="sub">working files: the repo, the artifacts, the half-finished output</text><path d="M145 176h350v60H145z" class="agent"></path><g class="icon"><path d="M172 197c-1.6-1.6-4.8-1.6-6.4 0-2.4 0-4 2.6-3.2 5-1.6.8-2.4 4-.8 5.6-.8 2.4.8 4.8 3.2 4.8.8 2.4 4 3.2 7.2 1.6ZM172 197c1.6-1.6 4.8-1.6 6.4 0 2.4 0 4 2.6 3.2 5 1.6.8 2.4 4 .8 5.6.8 2.4-.8 4.8-3.2 4.8-.8 2.4-4 3.2-7.2 1.6Z"></path></g><text x="196" y="202" class="title">memory/{agent}/{session}</text><text x="196" y="222" class="sub">conversation history, scratchpads, everything learned across sessions</text><path d="M145 248h350v60H145z" class="agent"></path><g class="icon"><path d="M168 268c-4 0-3 4-3 6 0 3-4 4-4 4s4 1 4 4c0 2-1 6 3 6M176 268c4 0 3 4 3 6 0 3 4 4 4 4s-4 1-4 4c0 2 1 6-3 6"></path></g><text x="196" y="274" class="title">runs/{id}</text><text x="196" y="294" class="sub">structured state: run metadata, task queue, tool results, config</text><path d="M145 362h350" style="stroke:#62feb5;stroke-width:1.2;stroke-dasharray:5 4;fill:none"></path><path d="M200 355v14M320 355v14M440 355v14" class="greentick"></path><text x="200" y="384" class="greenlabel" text-anchor="middle">t0</text><text x="320" y="384" class="greenlabel" text-anchor="middle">t1</text><text x="440" y="384" class="greenlabel" text-anchor="middle">t2</text><text x="320" y="410" style="font-size:13px;fill:#94a3b8" text-anchor="middle">
    Snapshots are the provenance: each tick is the whole world, restorable and forkable.
  </text></svg></div></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="today-that-world-is-scattered-across-five-systems">Today, that world is scattered across five systems<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#today-that-world-is-scattered-across-five-systems" class="hash-link" aria-label="Direct link to Today, that world is scattered across five systems" title="Direct link to Today, that world is scattered across five systems" translate="no">​</a></h2>
<p>That's the theory. In most real deployments the fourth layer looks nothing like
one namespace:</p>
<table><thead><tr><th>State</th><th>Where it sits today</th></tr></thead><tbody><tr><td>Conversation history</td><td>Postgres, in a <code>messages</code> table your framework created on first boot</td></tr><tr><td>Memory</td><td>Pinecone or pgvector for the embeddings, Redis for the hot cache, maybe Mem0, Zep, or Cognee layered on top</td></tr><tr><td>Checkpoints</td><td>your framework's checkpointer, such as <a class="" href="https://www.tigrisdata.com/blog/eval-agents-real-state/">the LangGraph one we built</a></td></tr><tr><td>Working files</td><td>an S3 bucket, copied in and out of the sandbox</td></tr><tr><td>Run metadata</td><td>wherever the orchestrator keeps it: Temporal's event history, a Celery result backend, a <code>runs</code> table you bolted on</td></tr></tbody></table>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" style="width:100%;height:auto" viewBox="0 0 640 400" role="img" aria-label="Diagram titled 'Five systems, ten seams'. Five circles, one per system: Postgres holding conversation history, Pinecone plus Redis holding memory, LangGraph holding checkpoints, an S3 bucket holding working files, and Temporal holding run metadata. Every pair of circles is connected by a dashed line, ten in total, each one a place where state can drift out of sync. Caption: no moment captures all five in one consistent state."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:13px;fill:#e2e8f0;font-weight:600}.sub{font-size:10.5px;fill:#94a3b8}.node,.seam{fill:#1a2e35;stroke:#2a3731;stroke-width:1}.seam{stroke:#94a3b8;stroke-dasharray:4 4;stroke-opacity:.35;fill:none}.icon{stroke:#e2e8f0;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;fill:none}</style><text x="320" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    Five systems, ten seams
  </text><text x="320" y="46" style="font-size:12px;fill:#94a3b8" text-anchor="middle">Every dashed line is a place where state can drift out of sync</text><path d="m130 135 195-23M130 135l385 5M130 135l75 165M130 135l325 165M325 112l190 28M325 112 205 300M325 112l130 188M515 140 205 300M515 140l-60 160M205 300h250" class="seam"></path><circle cx="130" cy="135" r="58" class="node"></circle><g class="icon"><path d="M119 104h22a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-11l-6 6v-6h-5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3M124 110h12M124 114h8"></path></g><text x="130" y="145" class="title" text-anchor="middle">Postgres</text><text x="130" y="161" class="sub" text-anchor="middle">conversation history</text><circle cx="325" cy="112" r="58" class="node"></circle><g class="icon"><path d="M325 79c-2-2-6-2-8 0-3 0-5 3-4 6-2 1-3 5-1 7-1 3 1 6 4 6 1 3 5 4 9 2ZM325 79c2-2 6-2 8 0 3 0 5 3 4 6 2 1 3 5 1 7 1 3-1 6-4 6-1 3-5 4-9 2Z"></path><path d="M319 85c2 1 2 4 0 5M331 85c-2 1-2 4 0 5"></path></g><text x="325" y="122" class="title" text-anchor="middle">Pinecone + Redis</text><text x="325" y="138" class="sub" text-anchor="middle">memory</text><circle cx="515" cy="140" r="58" class="node"></circle><g class="icon"><path d="M508 106v24M508 106l16 4-16 5Z"></path></g><text x="515" y="150" class="title" text-anchor="middle">LangGraph</text><text x="515" y="166" class="sub" text-anchor="middle">checkpoints</text><circle cx="205" cy="300" r="58" class="node"></circle><g class="icon" transform="translate(205 278)"><path d="m-11-5 3 15a3 2 0 0 0 3 2H5a3 2 0 0 0 3-2l3-15"></path><ellipse cy="-5" rx="11" ry="3"></ellipse><path d="M-8-7A8 7 0 0 1 8-7"></path></g><text x="205" y="310" class="title" text-anchor="middle">S3 bucket</text><text x="205" y="326" class="sub" text-anchor="middle">working files</text><circle cx="455" cy="300" r="58" class="node"></circle><g class="icon" transform="translate(455 278)"><rect width="9" height="8" x="-13" y="-12" rx="1.5"></rect><rect width="9" height="8" x="4" y="-12" rx="1.5"></rect><rect width="9" height="8" x="-4.5" y="4" rx="1.5"></rect><path d="m-8-4 7 8M8-4 1 4"></path></g><text x="455" y="310" class="title" text-anchor="middle">Temporal</text><text x="455" y="326" class="sub" text-anchor="middle">run metadata</text><text x="320" y="390" style="font-size:13px;fill:#94a3b8" text-anchor="middle">
    No moment captures all five in one consistent state.
  </text></svg></div></figure>
<p>Each piece is fine on its own; the problem is the seams, because there is no
moment at which you can capture all five systems in a consistent state. You
cannot snapshot the agent, because the agent has no snapshot boundary, and you
cannot fork it to try two continuations. You cannot replay Tuesday's run
byte-for-byte either, because Tuesday exists in five places that were never
consistent with each other in the first place.</p>
<p>Consolidating the world into one bucket is a real trade, and it's worth naming:
you give up SQL over your run metadata for atomicity over your agent. If your
access pattern is get, put, and list (and for agent state it almost always is),
that trade is heavily in your favor. If you need rich queries over metadata,
keep a queryable index and treat the bucket as the source of truth it's derived
from.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="but-my-sandbox-has-snapshots">"But my sandbox has snapshots"<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#but-my-sandbox-has-snapshots" class="hash-link" aria-label="Direct link to &quot;But my sandbox has snapshots&quot;" title="Direct link to &quot;But my sandbox has snapshots&quot;" translate="no">​</a></h2>
<p>It does, and the objection deserves a direct answer:
<a href="https://www.daytona.io/" target="_blank" rel="noopener noreferrer" class="">Daytona</a> snapshots and volumes do persist state across
sandbox restarts, and if persistence were the whole requirement, you could stop
there. This isn't a knock on Daytona either; their own docs treat Tigris as a
<a href="https://www.daytona.io/docs/mount-external-storage#mount-a-tigris-bucket" target="_blank" rel="noopener noreferrer" class="">first-class mount target</a>
precisely because the two layers are built to split the work this way: Daytona
owns the compute, the bucket owns the world. Each layer checkpoints what it
owns: Daytona checkpoints the machine your agent runs on, and the bucket
checkpoints the agent itself, the whole world as one point-in-time unit you can
restore, branch, and compare.</p>
<p>The requirement is forkability, and the difference is not subtle. A sandbox
snapshot is a photograph of a machine: it restores exactly one state, and it
hauls the full image around every time.</p>
<p>A bucket fork is a branch of a world. On Tigris a fork is
<a href="https://www.tigrisdata.com/docs/snapshots/" target="_blank" rel="noopener noreferrer" class="">copy-on-write</a>: metadata rather
than bytes. You can branch a 40 TB world for exactly what a 40 MB one costs, and
branch it fifty times for the price of once. The fork outlives every sandbox,
and because it's just a bucket, you can read it from any cloud or provider with
zero egress. You want the machine frozen approximately never; you want the world
branched constantly: before every risky action, for every parallel variant, at
the start of every eval run.</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="640" height="348" style="width:100%;height:auto" viewBox="0 0 640 348" role="img" aria-label="Diagram titled 'One fork per run'. The agent-world Tigris Bucket forks zero-copy into run-a and run-b. Each fork is mounted by its own fresh, disposable sandbox. Dashed green arrows loop from each run back to the base bucket, labeled 'merge or discard': the winning fork's changes are promoted, the rest are deleted."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:13px;fill:#e2e8f0;font-weight:600}.sub{font-size:11px;fill:#94a3b8}.arrowlabel,.greenlabel{font-size:10px;fill:#94a3b8}.greenlabel{fill:#62feb5}.agent,.eph{rx:6;ry:6;fill:#1a2e35;stroke:#2a3731;stroke-width:1}.eph{stroke:#94a3b8;stroke-dasharray:5 4;stroke-opacity:.6}.greenline,.line{stroke:#94a3b8;stroke-width:1.2;fill:none}.greenline{stroke:#62feb5;stroke-dasharray:5 4}</style><defs><marker id="fArrowGray" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#94a3b8" d="m0 0 10 5-10 5z"></path></marker><marker id="fArrowGreen" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#62feb5" d="m0 0 10 5-10 5z"></path></marker></defs><text x="320" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    One fork per run
  </text><text x="320" y="46" style="font-size:12px;fill:#94a3b8" text-anchor="middle">Every sandbox mounts a private fork; the harness merges or discards it</text><path d="M30 163h160v70H30z" style="rx:6px;ry:6px;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.45);stroke-width:1.5"></path><text x="110" y="193" style="font-size:14px;fill:#62feb5;font-weight:600" text-anchor="middle">Tigris Bucket</text><text x="110" y="212" class="sub" text-anchor="middle">agent-world · the base</text><path d="M280 83h120v50H280z" class="agent"></path><text x="340" y="106" class="title" text-anchor="middle">run-a</text><text x="340" y="122" class="sub" text-anchor="middle">fork</text><path d="M280 263h120v50H280z" class="agent"></path><text x="340" y="286" class="title" text-anchor="middle">run-b</text><text x="340" y="302" class="sub" text-anchor="middle">fork</text><path marker-end="url(#fArrowGray)" d="m192 181 86-67" class="line"></path><text x="224" y="136" class="arrowlabel" text-anchor="middle">fork · zero-copy</text><path marker-end="url(#fArrowGray)" d="m192 215 86 67" class="line"></path><text x="224" y="268" class="arrowlabel" text-anchor="middle">fork · zero-copy</text><path d="M470 83h140v50H470z" class="eph"></path><text x="540" y="106" class="title" text-anchor="middle">fresh sandbox A</text><text x="540" y="122" class="sub" text-anchor="middle">disposable</text><path d="M470 263h140v50H470z" class="eph"></path><text x="540" y="286" class="title" text-anchor="middle">fresh sandbox B</text><text x="540" y="302" class="sub" text-anchor="middle">disposable</text><path marker-end="url(#fArrowGray)" d="M402 108h66" class="line"></path><text x="435" y="100" class="arrowlabel" text-anchor="middle">mounts</text><path marker-end="url(#fArrowGray)" d="M402 288h66" class="line"></path><text x="435" y="280" class="arrowlabel" text-anchor="middle">mounts</text><path marker-end="url(#fArrowGreen)" d="M300 81C190 56 78 96 104 160" class="greenline"></path><text x="170" y="72" class="greenlabel" text-anchor="middle">merge or discard</text><path marker-end="url(#fArrowGreen)" d="M300 315c-110 25-222-15-196-79" class="greenline"></path><text x="170" y="336" class="greenlabel" text-anchor="middle">merge or discard</text></svg></div></figure>
<p>The loop is always the same three moves: fork the world, work against the fork,
promote the winner.</p>
<p>That loop is where fleets live or die. A platform running a thousand concurrent
agents takes a thousand forks before lunch, and zero-copy is what makes that a
non-event: creating a fork doesn't scale with world size, a fork stores only
what its run actually writes, and deleting a dead one frees exactly those
deltas. Per-run isolation at fleet scale is an economics problem before it's an
engineering one, and copy-on-write answers both. We've written before about
<a class="" href="https://www.tigrisdata.com/blog/fifty-agents-one-bucket/">fifty agents sharing one bucket</a>; one fork per run is
the same idea with the blast radius set to zero.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-shim-boot-the-sandbox-already-bound-to-its-fork">The shim: boot the sandbox already bound to its fork<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#the-shim-boot-the-sandbox-already-bound-to-its-fork" class="hash-link" aria-label="Direct link to The shim: boot the sandbox already bound to its fork" title="Direct link to The shim: boot the sandbox already bound to its fork" translate="no">​</a></h2>
<p>Once the world is one bucket, the lifecycle writes itself: fork before the run,
mount the fork in the sandbox, merge or discard after. You wire it up with three
small pieces: a few lines in your harness that fork the world and mint
credentials scoped to that fork, an entrypoint wrapper around the
<a href="https://www.tigrisdata.com/docs/cli/" target="_blank" rel="noopener noreferrer" class="">Tigris CLI</a> that binds the sandbox to its
fork before your agent's first instruction runs, and the
<a href="https://github.com/storagesdk/storagesdk" target="_blank" rel="noopener noreferrer" class="">storagesdk</a> MCP server that lets your
agent work against the fork without copying it.</p>
<p>Before the code, a quick sketch of how <a href="https://www.daytona.io/" target="_blank" rel="noopener noreferrer" class="">Daytona</a> works,
because the shim leans on it. A Daytona sandbox is a full computer with its own
kernel, filesystem, and network stack, created from a snapshot image you define
once in code and ready to execute in under 90 milliseconds. The
<a href="https://www.daytona.io/docs" target="_blank" rel="noopener noreferrer" class="">Python and TypeScript SDKs</a> drive the whole
lifecycle from your harness: build the image, create the sandbox, exec the
process, destroy it. We pair with Daytona because the economics match:
fork-per-run only pays off if both halves of the loop are cheap, and a zero-copy
fork plus a sub-100ms sandbox means per-run isolation costs milliseconds on the
storage axis and on the compute axis alike. One layer makes the computer
disposable, the other makes the world durable, and neither has to pretend to be
the other.</p>
<p>The harness piece comes first, and it's where the isolation becomes enforceable:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain"># In the harness, before the sandbox boots.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">FORK="run-${RUN_ID}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 1. Fork the world. Copy-on-write, metadata-only:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    constant-time whether the bucket holds megabytes or terabytes.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets create "$FORK" --fork-of agent-world</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 2. Mint a key that can see only the fork. Capture the key ID and</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    secret from the output; the secret is shown exactly once.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris access-keys create "sandbox-${RUN_ID}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris access-keys assign "$KEY_ID" -b "$FORK" -r Editor</span><br></div></code></pre></div></div>
<p>Pass the fork name and the scoped key into the sandbox's environment when you
create it. In Daytona's Python SDK that's one call:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># Still in the harness. Daytona() reads DAYTONA_API_KEY; fork,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># key_id, and key_secret were captured from the tigris commands above.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> daytona </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> CreateSandboxFromSnapshotParams</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> Daytona</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">sandbox </span><span class="token operator">=</span><span class="token plain"> Daytona</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">create</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    CreateSandboxFromSnapshotParams</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        snapshot</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runtime"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># your image, entrypoint baked in</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        env_vars</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token string" style="color:rgb(255, 121, 198)">"FORK"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token string" style="color:rgb(255, 121, 198)">"TIGRIS_ACCESS_KEY_ID"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> key_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token string" style="color:rgb(255, 121, 198)">"TIGRIS_SECRET_ACCESS_KEY"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> key_secret</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Those three variables are the only credentials the sandbox ever holds, which
means the base bucket isn't hidden from your agent; it's unreachable: a prompt
injection, a buggy tool, or a model on a bad day can name <code>agent-world</code> all it
wants and the API will refuse it. Inside the snapshot, the entrypoint does the
rest:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">#!/usr/bin/env bash</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># entrypoint.sh, baked into the Daytona snapshot. The sandbox</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># arrives holding fork-scoped credentials and nothing else.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">set -euo pipefail</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 0. The harness set these three at sandbox creation. If any are</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    missing, fail loudly now rather than partway through a run.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">: "${FORK:?}" "${TIGRIS_ACCESS_KEY_ID:?}" "${TIGRIS_SECRET_ACCESS_KEY:?}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 1. The scoped key arrives as TIGRIS_ACCESS_KEY_ID and</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    TIGRIS_SECRET_ACCESS_KEY (what storagesdk reads); mirror it</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    to the AWS names the Tigris CLI reads.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">export AWS_ACCESS_KEY_ID="${TIGRIS_ACCESS_KEY_ID}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">export AWS_SECRET_ACCESS_KEY="${TIGRIS_SECRET_ACCESS_KEY}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 2. Pull the working set only. Memory, run metadata, and artifacts</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    stay in the fork and are read by key, never copied into the VM.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris cp -r "t3://${FORK}/workspace/" /workspace/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># 3. Bind every storage tool to the fork. The MCP server below and</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#    any `storage` command the agent runs see the fork, not the base.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">export STORAGE_ADAPTER=tigris</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">export TIGRIS_BUCKET="${FORK}"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">exec "$@"</span><br></div></code></pre></div></div>
<p>Set it as the snapshot's entrypoint and the agent process (<code>node agent.js</code>,
<code>python -m agent</code>, whatever you run) inherits a world that is entirely its own.
The worst run your agent will ever have is scoped to a fork you were always free
to throw away.</p>
<p>If you'd rather not copy even the working set, Daytona documents
<a href="https://www.daytona.io/docs/mount-external-storage#mount-a-tigris-bucket" target="_blank" rel="noopener noreferrer" class="">mounting a Tigris bucket straight into the sandbox</a>
with <code>mount-s3</code> and the <code>https://t3.storage.dev</code> endpoint. Bake <code>mount-s3</code> into
your Daytona snapshot, point the mount at the fork, and the whole world shows up
as a local directory, so your agent's existing file tools work against the fork
without a byte pulled up front. The fork-scoped key works unchanged, because
<code>mount-s3</code> reads the same <code>AWS_*</code> variables the entrypoint already exports.</p>
<p>The second piece is how your agent touches the rest of its world: the memory,
run metadata, and artifacts the entrypoint deliberately didn't copy, because
copying a terabyte world into a microVM would throw away the point of a
zero-copy fork. Instead, your agent reads and writes them by key through
storagesdk, whose MCP server exposes every storage verb as a tool. Install it
when you build the Daytona snapshot (<code>npm install -g @storagesdk/cli</code>) and add
one entry to the agent's MCP config:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"mcpServers"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token property">"world"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"command"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"storage"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token property">"args"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"mcp"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Because the entrypoint exported <code>TIGRIS_BUCKET</code> as the fork, every tool the
server offers (<code>download</code>, <code>upload</code>, <code>ls</code>, <code>snapshot_create</code>) is already aimed
at the fork, and the fork-scoped key is what makes the aim binding. Your agent
can also checkpoint its own world before a risky migration and roll back if the
result looks wrong, which is a nice trick, but don't build your safety story on
the model remembering to do it. The guarantees live at the boundaries: the fork
the entrypoint created, and the snapshot the harness takes before promotion.</p>
<p>When the run ends, you decide what the fork was worth, and you decide it from
the harness, not the sandbox:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain"># Run passed validation: promote the fork's changes back to the base.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris cp -r "t3://run-${RUN_ID}/workspace/" "t3://agent-world/workspace/"</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># Run failed, or was an experiment you're done with: drop the fork.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># Deleting a fork frees only the deltas the run actually wrote.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris rm "t3://run-${RUN_ID}" -f</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"># Either way, the run's credentials die with the run.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris access-keys delete "$KEY_ID" --yes</span><br></div></code></pre></div></div>
<p>One <code>sandbox.delete()</code> in the same harness retires the compute the same way. Be
honest about the asymmetry here: the fork was free, and promotion isn't. It's a
copy, the one step in the loop that costs what it weighs, so promote the
prefixes the run actually changed rather than the whole fork.</p>
<p>If you want provenance on top, enable snapshots on the base bucket and take one
before each promotion with <code>tigris snapshots take agent-world</code>, and "what did
the world look like before run 4187" becomes a timestamp you can fork from.</p>
<p>Every disposable layer sits above the line, and the one layer you can't rebuild
sits below it, forkable:</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="640" height="375" style="width:100%;height:auto" viewBox="0 0 640 375" role="img" aria-label="Diagram titled 'Where the agent actually lives'. Top row, drawn with dashed borders because it's rebuildable: the model at the inference provider, the harness (your loop), and the Daytona sandbox (built to be killed). The harness calls the model and dispatches the sandbox. Below the split sits one Tigris Bucket holding working files, memory, structured state, and provenance, labeled 'the world, the only layer you can't rebuild'. The sandbox writes state down into the bucket and mounts a fork back up from it."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:14px;fill:#e2e8f0;font-weight:600}.arrowlabel,.sub{font-size:11px;fill:#94a3b8}.arrowlabel{font-size:10px}.eph,.line{stroke:#94a3b8}.eph{rx:6;ry:6;fill:#1a2e35;stroke-width:1;stroke-dasharray:5 4;stroke-opacity:.6}.line{stroke-width:1.2}</style><defs><marker id="arrowGray" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#94a3b8" d="m0 0 10 5-10 5z"></path></marker><marker id="arrowGreen" markerHeight="7" markerWidth="7" orient="auto-start-reverse" refX="9" refY="5" viewBox="0 0 10 10"><path fill="#62feb5" d="m0 0 10 5-10 5z"></path></marker></defs><text x="320" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    Where the agent actually lives
  </text><text x="320" y="46" style="font-size:12px;fill:#94a3b8" text-anchor="middle">Kill anything in the top row and nothing is lost; the bucket can't be rebuilt</text><path d="M30 70h150v64H30z" class="eph"></path><text x="105" y="96" class="title" text-anchor="middle">Model</text><text x="105" y="116" class="sub" text-anchor="middle">inference provider</text><path d="M245 70h150v64H245z" class="eph"></path><text x="320" y="96" class="title" text-anchor="middle">Harness</text><text x="320" y="116" class="sub" text-anchor="middle">your loop</text><path d="M460 70h150v64H460z" class="eph"></path><text x="535" y="96" class="title" text-anchor="middle">Sandbox</text><text x="535" y="116" class="sub" text-anchor="middle">built to be killed</text><path marker-end="url(#arrowGray)" d="M243 102h-57" class="line"></path><text x="214" y="93" class="arrowlabel" text-anchor="middle">calls</text><path marker-end="url(#arrowGray)" d="M397 102h57" class="line"></path><text x="426" y="93" class="arrowlabel" text-anchor="middle">dispatches</text><path marker-end="url(#arrowGray)" d="m520 136-95 116" class="line"></path><text x="500" y="199" class="arrowlabel">writes state</text><path marker-end="url(#arrowGreen)" d="m390 252 97-116" style="stroke:#62feb5;stroke-width:1.2;stroke-dasharray:5 4"></path><text x="392" y="199" class="arrowlabel" text-anchor="end">mounts a fork</text><path d="M145 254h350v70H145z" style="rx:6px;ry:6px;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.45);stroke-width:1.5"></path><text x="320" y="282" style="font-size:15px;fill:#62feb5;font-weight:600" text-anchor="middle">Tigris Bucket</text><text x="320" y="304" class="sub" text-anchor="middle">
    working files · memory · structured state · provenance
  </text><text x="320" y="354" style="font-size:13px;fill:#62feb5;font-weight:600" text-anchor="middle">
    The world: the only layer you can't rebuild
  </text></svg></div></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-bucket-does-and-doesnt">What the bucket does, and doesn't<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#what-the-bucket-does-and-doesnt" class="hash-link" aria-label="Direct link to What the bucket does, and doesn't" title="Direct link to What the bucket does, and doesn't" translate="no">​</a></h2>
<p>One bucket doesn't run anything: you still bring the harness, the model, and the
sandboxes, and the bucket is the state they orbit rather than a replacement for
any of them. It doesn't merge divergent forks for you either: deciding which
run's changes deserve promotion is your validator's job, and the fork only makes
that decision cheap to act on.</p>
<p>It's also not the first tool to reach for in every deployment. If your agent's
entire world is a git repo, git is already a forkable, snapshotted store, and if
the world is a few megabytes, a <code>runs/{id}/</code> prefix convention inside one bucket
buys most of the isolation with none of the new moving parts. The bucket earns
its keep when the world spans files plus memory plus metadata (git only covers
the first), when it's big enough that copying is the bottleneck, or when runs
you don't fully trust are mutating state other runs depend on.</p>
<p>What it does give you is the one property the scattered version can't: a
snapshot boundary around the whole world. Files, memory, structured state, and
provenance move as one unit: forked together, promoted together, deleted
together. Every operation in this post falls out of that single fact.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hand-this-prompt-to-your-agent">Hand this prompt to your agent<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#hand-this-prompt-to-your-agent" class="hash-link" aria-label="Direct link to Hand this prompt to your agent" title="Direct link to Hand this prompt to your agent" translate="no">​</a></h2>
<p>If your agent builds itself (and whose doesn't, lately), here's the setup as a
prompt instead of a runbook. Paste it into Claude Code or the coding agent of
your choice:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Set up durable, forkable state for my agent fleet on Tigris.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Assume the tigris CLI is installed and authenticated, and my</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">harness already creates Daytona sandboxes; wire this into it.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">1. Create the base bucket:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `tigris buckets create agent-world --enable-snapshots`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">2. Lay out the namespace: `workspace/` for working files,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `memory/{agent}/{session}` for memory, `runs/{id}` for run</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   metadata as JSON values under keys (object storage as KV:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   no separate database, no pointer pattern).</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">3. In the harness, before each sandbox boots: fork the base</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   (`tigris buckets create run-${RUN_ID} --fork-of agent-world`),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   mint a key scoped to the fork (`tigris access-keys create`,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   capture the printed ID and secret, then</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `tigris access-keys assign $KEY_ID -b run-${RUN_ID} -r Editor`),</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   and pass them into the sandbox env (env_vars on the Daytona</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   SDK's create call) as FORK, TIGRIS_ACCESS_KEY_ID, and</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   TIGRIS_SECRET_ACCESS_KEY, its only credentials.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">4. Write an entrypoint.sh that mirrors the two TIGRIS_* vars to</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, pulls the</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   working set with `tigris cp -r t3://${FORK}/workspace/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   /workspace/`, and exports STORAGE_ADAPTER=tigris and</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   TIGRIS_BUCKET set to the fork. Bake it into my Daytona</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   snapshot as the entrypoint, alongside</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `npm install -g @storagesdk/cli`.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">5. Register `storage mcp` in my agent's MCP config so it reads</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   and writes memory, run metadata, and artifacts by key against</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   the fork instead of copying them.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">6. After each run, in the harness: if validation passes, take</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `tigris snapshots take agent-world`, then promote with</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `tigris cp -r t3://run-${RUN_ID}/workspace/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   t3://agent-world/workspace/`; otherwise drop the fork with</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   `tigris rm t3://run-${RUN_ID} -f`. Either way, delete the</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">   run's access key: `tigris access-keys delete $KEY_ID --yes`.</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="an-agent-you-cant-fork-is-an-agent-you-cant-trust">An agent you can't fork is an agent you can't trust<a href="https://www.tigrisdata.com/blog/where-does-the-agent-live/#an-agent-you-cant-fork-is-an-agent-you-cant-trust" class="hash-link" aria-label="Direct link to An agent you can't fork is an agent you can't trust" title="Direct link to An agent you can't fork is an agent you can't trust" translate="no">​</a></h2>
<p>So, where does the agent live?</p>
<p>Your agent runs in a <a href="https://www.daytona.io/" target="_blank" rel="noopener noreferrer" class="">Daytona</a> sandbox, calls a model,
and takes orders from a harness, and all three can vanish mid-task without
costing you anything but a restart. The one layer that can't be rebuilt is the
bucket. Treat that bucket as the agent, and the operations you've been missing
fall out for free: snapshot it and you can replay any run, fork it and you can
try anything, delete a fork and the worst run your agent ever had never
happened. And the pattern doesn't change between one agent and a thousand: a
fleet is just more forks of more worlds, each one scoped, cheap, and disposable.
That's where the agent lives, so make it forkable.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Forking and snapshots on Tigris</span><p>Copy-on-write forks of your agent's world: constant-time at any size, zero egress fees across clouds.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/snapshots/" class="cta-link"><div>Read the docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>agents</category>
            <category>storage</category>
        </item>
        <item>
            <title><![CDATA[Every Tenant Has a Past: Evaluating LangGraph Agents]]></title>
            <link>https://www.tigrisdata.com/blog/eval-agents-real-state/</link>
            <guid>https://www.tigrisdata.com/blog/eval-agents-real-state/</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Fork a customer's entire LangGraph agent state, replay their conversations through a prompt change, and judge it head-to-head before you ship to prod.]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-e1993202d8559fb1a56a7563aec98d0b.webp" class="hero-image" alt="Fork your whole agent and eval a change against real state, LangGraph checkpointing on Tigris">
<p>The last time I shipped a "small" prompt change to a production agent, it passed
every test I had and made the agent worse. It got a little more generic, a
little more likely to ask a customer something they'd already told it. My tests
didn't catch it because my tests were three hand-written threads, and three
hand-written threads have no past. The behavior that mattered only showed up
against customers with <em>history</em>, and history is exactly what a fixture doesn't
have.</p>
<p>If you run an agent <em>platform</em>, that's not your problem once. It's every
tenant's problem, multiplied. Each agent accumulates state you don't own and
can't recreate, and your job is to keep all of them in line as you change them.
To know a change actually helps, you have to eval(uate) it against the real,
accumulated world the agent lives in. Then you can hand that workflow to your
users, but only if their state lives somewhere you can branch.</p>
<p>This is an exercise in building a small harness that answers one question
without touching production: will this change make my agent better or worse
against real users?</p>
<p>All the code for this example lives here:
<a href="https://github.com/tigrisdata/tigris-langgraph/tree/main/examples/eval-on-real-state" target="_blank" rel="noopener noreferrer" class=""><code>examples/eval-on-real-state</code></a>.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-a-state-blind-regression-looks-like">What a state-blind regression looks like<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#what-a-state-blind-regression-looks-like" class="hash-link" aria-label="Direct link to What a state-blind regression looks like" title="Direct link to What a state-blind regression looks like" translate="no">​</a></h2>
<p>Here's what one of your tenants' customers actually sees after the change ships:</p>
<figure class="mermaidFrame_PGZu" style="max-width:34rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="600" height="300" style="width:100%;height:auto" viewBox="0 0 600 300" role="img" aria-label="A support chat thread. Earlier in the thread the customer says 'I'm vegetarian, with a severe tree-nut allergy' and the agent replies 'Noted, I'll keep that on your profile.' Today the customer asks 'Suggest a dinner from this week's menu' and the agent replies 'Sure! Any allergies or dietary preferences?' — re-asking a fact the customer already gave. A note reads: the customer already told it, the stored state went unused."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.tag{font-size:11px;fill:#94a3b8}.msg{font-size:12px;fill:#e2e8f0}.av{font-size:11px;font-weight:600}.user-bubble{rx:9;ry:9;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.4);stroke-width:1}.av-agent,.av-user{fill:#15242b;stroke-width:1}.av-user{stroke:rgba(98,254,181,.4)}.av-agent{stroke:#3a4a44}</style><text x="300" y="28" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    One tweak later, the agent forgets the customer
  </text><text x="300" y="54" class="tag" text-anchor="middle">
    — earlier in the thread —
  </text><path d="M290 64h264v34H290z" class="user-bubble"></path><text x="302" y="85" class="msg">I'm vegetarian, with a severe tree-nut allergy.</text><circle cx="574" cy="81" r="11" class="av-user"></circle><text x="574" y="85" fill="#62feb5" class="av" text-anchor="middle">C</text><path d="M46 108h246v34H46z" style="rx:9px;ry:9px;fill:#1a2e35;stroke:#2a3731;stroke-width:1"></path><text x="58" y="129" class="msg">Noted. I'll keep that on your profile.</text><circle cx="28" cy="125" r="11" class="av-agent"></circle><text x="28" y="129" fill="#bac1be" class="av" text-anchor="middle">A</text><text x="300" y="168" class="tag" text-anchor="middle">— today —</text><path d="M300 178h254v34H300z" class="user-bubble"></path><text x="312" y="199" class="msg">Suggest a dinner from this week's menu.</text><circle cx="574" cy="195" r="11" class="av-user"></circle><text x="574" y="199" fill="#62feb5" class="av" text-anchor="middle">C</text><path d="M46 222h282v34H46z" style="rx:9px;ry:9px;fill:rgba(248,113,113,.1);stroke:rgba(248,113,113,.45);stroke-width:1"></path><text x="58" y="243" style="font-size:12px;fill:#f87171">
    Sure! Any allergies or dietary preferences?
  </text><circle cx="28" cy="239" r="11" class="av-agent"></circle><text x="28" y="243" fill="#bac1be" class="av" text-anchor="middle">A</text><text x="58" y="278" style="font-size:11px;fill:#f87171">
    ↳ the customer already told it. the stored state went unused.
  </text></svg></div></figure>
<p>Ana told the agent weeks ago that she's vegetarian with a severe tree-nut
allergy, and that fact now lives in her thread. When she asks for "a dinner from
this week's menu," the old prompt answered straight from memory with "a
vegetarian, nut-free pick," but my "small" edit made the agent a little more
generic and it started replying "any allergies or preferences?", re-asking
something Ana had already told it.</p>
<p>The reason my tests missed it is the whole point. On a blank fixture there is no
prior turn to recall, so <em>both</em> the old and new prompt ask the same clarifying
question and look identical. The regression only exists where there's a past to
forget, and a fixture has none.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-whole-loop-on-one-page">The whole loop on one page<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#the-whole-loop-on-one-page" class="hash-link" aria-label="Direct link to The whole loop on one page" title="Direct link to The whole loop on one page" translate="no">​</a></h2>
<p>The whole thing is four steps: fork prod into a throwaway bucket, replay real
threads through the candidate, score it against the baseline, then drop the
fork. The trick that makes it practical is something you can only do on object
storage: forking the bucket. When you fork a bucket holding agent data, you
clone the entire agent: every thread, checkpoint, and message. The clone is made
by reference, so it lands instantly no matter how much history has accumulated.</p>
<figure class="mermaidFrame_PGZu" style="max-width:40rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="680" height="372" style="width:100%;height:auto" viewBox="0 0 680 372" role="img" aria-label="The prod bucket holds real accumulated state and is never written to. It forks into a baseline fork (current prompt) and a candidate fork (new prompt). Each fork replays the real customer threads in isolation. The two sets of replies go to a pairwise, order-swapped judge, which produces a ship-or-hold verdict plus a memory-recall signal. Both forks are then dropped with no residue."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.label{font-size:14px;fill:#e2e8f0;font-weight:600}.small{font-size:11.5px;fill:#bac1be}.glabel{font-size:11px;fill:#62feb5;font-weight:600}.fork{rx:6;ry:6;fill:#1a2e35;stroke:#2a3731;stroke-width:1}.flow{stroke:#3a4a44;stroke-width:1.5;fill:none}</style><defs><marker id="arrow" markerHeight="9" markerWidth="9" orient="auto" refX="6" refY="3"><path fill="#3a4a44" d="m0 0 6 3-6 3Z"></path></marker></defs><text x="340" y="30" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    Fork prod, replay, judge, discard
  </text><path d="M250 56h180v44H250z" style="rx:8px;ry:8px;fill:#142229;stroke:#62feb5;stroke-opacity:0.6;stroke-width:1.5"></path><text x="340" y="83" class="label" text-anchor="middle">prod bucket</text><path marker-end="url(#arrow)" d="m300 100-116 46" class="flow"></path><path marker-end="url(#arrow)" d="m380 100 116 46" class="flow"></path><text x="214" y="122" class="glabel" text-anchor="middle">fork()</text><text x="466" y="122" class="glabel" text-anchor="middle">fork()</text><path d="M90 148h180v46H90z" class="fork"></path><text x="180" y="171" class="label" text-anchor="middle">baseline fork</text><text x="180" y="186" class="small" text-anchor="middle">current prompt</text><path d="M410 148h180v46H410z" class="fork"></path><text x="500" y="171" class="label" text-anchor="middle">candidate fork</text><text x="500" y="186" class="small" text-anchor="middle">new prompt</text><text x="180" y="216" class="small" text-anchor="middle">
    replay real threads
  </text><text x="500" y="216" class="small" text-anchor="middle">
    replay real threads
  </text><path marker-end="url(#arrow)" d="m180 222 120 20" class="flow"></path><path marker-end="url(#arrow)" d="m500 222-120 20" class="flow"></path><path d="M250 244h180v44H250z" style="rx:6px;ry:6px;fill:#15242b;stroke:#3a4a44;stroke-width:1"></path><text x="340" y="266" class="label" text-anchor="middle">pairwise judge</text><text x="340" y="281" class="small" text-anchor="middle">order-swapped</text><path marker-end="url(#arrow)" d="M340 288v24" class="flow"></path><path d="M208 314h264v44H208z" style="rx:6px;ry:6px;fill:rgba(98,254,181,.12);stroke:rgba(98,254,181,.45);stroke-width:1.2"></path><text x="340" y="341" class="label" text-anchor="middle">
    VERDICT: ship / hold
  </text></svg></div></figure>
<p>The checkpointer is
<a href="https://pypi.org/project/langgraph-checkpoint-tigris/" target="_blank" rel="noopener noreferrer" class=""><code>langgraph-checkpoint-tigris</code></a>,
a drop-in <code>BaseCheckpointSaver</code> that stores each checkpoint as an object in a
Tigris bucket. Install it and let's build each box in that diagram.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip install -U langgraph-checkpoint-tigris</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-an-agent-whose-only-variable-is-the-change-under-test">1. An agent whose only variable is the change under test<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#1-an-agent-whose-only-variable-is-the-change-under-test" class="hash-link" aria-label="Direct link to 1. An agent whose only variable is the change under test" title="Direct link to 1. An agent whose only variable is the change under test" translate="no">​</a></h3>
<p>Hold everything fixed except the thing you're evaluating: the graph, the model,
and the accumulated thread history all stay the same, so any difference in the
verdict is attributable to your change. Here the change is the system prompt,
prepended at call time so it never gets baked into the stored thread. That is
what lets us replay the <em>same</em> real conversation under two different prompts.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> langchain</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">chat_models </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> init_chat_model</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> langgraph</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">graph </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> START</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> MessagesState</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> StateGraph</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">BASELINE_PROMPT </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"You are a helpful customer support assistant. Answer the user's question."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">CANDIDATE_PROMPT </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"You are a helpful customer support assistant. Before answering, recall "</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"everything you already know about THIS customer from the conversation so "</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"far (their plan, preferences, constraints, and past issues) and tailor "</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"your answer to it. Never ask them to repeat something they've told you."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">build_agent</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">system_prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> temperature</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">float</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">0.2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">-</span><span class="token operator">&gt;</span><span class="token plain"> StateGraph</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    model </span><span class="token operator">=</span><span class="token plain"> init_chat_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"claude-haiku-4-5-20251001"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> temperature</span><span class="token operator">=</span><span class="token plain">temperature</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">call_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">state</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> MessagesState</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">-</span><span class="token operator">&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">dict</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        messages </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"role"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"system"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"content"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> system_prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain">state</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"messages"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"messages"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">invoke</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    builder </span><span class="token operator">=</span><span class="token plain"> StateGraph</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">MessagesState</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    builder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">add_node</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"call_model"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> call_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    builder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">add_edge</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">START</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"call_model"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> builder</span><br></div></code></pre></div></div>
<p>The temperature is low on purpose. We want to measure the prompt's effect, not
sampling noise. That candidate prompt is the kind of one-liner that looks like
nothing on a fixture and proves itself only against memory-rich threads.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-the-real-state-you-evaluate-against">2. The real state you evaluate against<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#2-the-real-state-you-evaluate-against" class="hash-link" aria-label="Direct link to 2. The real state you evaluate against" title="Direct link to 2. The real state you evaluate against" translate="no">​</a></h3>
<p>Each thread is one real customer conversation. It carries the memory we seed in
its history, a held-out probe we score both variants on, and the keywords that
prove a reply actually used the memory.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> dataclasses </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> dataclass</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> field</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token decorator annotation punctuation" style="color:rgb(248, 248, 242)">@dataclass</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">Thread</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    history</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">list</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain">                 </span><span class="token comment" style="color:rgb(98, 114, 164)"># prior turns that establish the memory</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    probe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token plain">                         </span><span class="token comment" style="color:rgb(98, 114, 164)"># the held-out question we score on</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    recall_markers</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">list</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token builtin" style="color:rgb(189, 147, 249)">str</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> field</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">default_factory</span><span class="token operator">=</span><span class="token builtin" style="color:rgb(189, 147, 249)">list</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># proof of recall</span><br></div></code></pre></div></div>
<p>In the demo we seed four of them, each holding one fact a good answer has to
use. The marker is a deliberately simple substring check: it is not the score,
it just makes the lesson concrete.</p>
<figure class="mermaidFrame_PGZu" style="max-width:38rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="600" height="356" style="width:100%;height:auto" viewBox="0 0 600 356" role="img" aria-label="Four seeded threads, each hiding one fact: cust-ana knows vegetarian and a severe tree-nut allergy and asks for a dinner suggestion; cust-ben knows Pro plan and Tokyo time zone and asks about a first response tonight; cust-cleo is a total beginner and asks how to set up automated backups; cust-dan had a past double-charge fixed with a credit and asks about another charge."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.tid{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;font-size:12.5px;fill:#62feb5;font-weight:600}.lbl{font-size:10.5px;fill:#94a3b8}.probe,.val{font-size:11.5px;fill:#e2e8f0}.probe{fill:#bac1be;font-style:italic}.card{rx:7;ry:7;fill:#142229;stroke:#2a3731;stroke-width:1}.markertext{font-size:10.5px;fill:#62feb5}</style><text x="300" y="26" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    Four seeded threads, each hiding one fact
  </text><path d="M20 44h560v64H20z" class="card"></path><text x="36" y="72" class="tid">cust-ana</text><text x="36" y="96" class="markertext">marker: vegetarian, nut</text><text x="180" y="70" class="val"><tspan class="lbl">knows </tspan>vegetarian, severe tree-nut allergy
  </text><text x="180" y="94" class="probe"><tspan class="lbl" font-style="normal">asks </tspan>“suggest a dinner from
    this week's menu”
  </text><path d="M20 118h560v64H20z" class="card"></path><text x="36" y="146" class="tid">cust-ben</text><text x="36" y="170" class="markertext">marker: pro</text><text x="180" y="144" class="val"><tspan class="lbl">knows </tspan>Pro plan, based in Tokyo (JST)
  </text><text x="180" y="168" class="probe"><tspan class="lbl" font-style="normal">asks </tspan>“when will I get a first
    response tonight?”
  </text><path d="M20 192h560v64H20z" class="card"></path><text x="36" y="220" class="tid">cust-cleo</text><text x="36" y="244" class="markertext">marker: step</text><text x="180" y="218" class="val"><tspan class="lbl">knows </tspan>total beginner, wants no jargon
  </text><text x="180" y="242" class="probe"><tspan class="lbl" font-style="normal">asks </tspan>“how do I set up automated
    backups?”
  </text><path d="M20 266h560v64H20z" class="card"></path><text x="36" y="294" class="tid">cust-dan</text><text x="36" y="318" class="markertext">marker: credit</text><text x="180" y="292" class="val"><tspan class="lbl">knows </tspan>past double-charge fixed with a credit
  </text><text x="180" y="316" class="probe"><tspan class="lbl" font-style="normal">asks </tspan>“another charge I don't
    recognize, what now?”
  </text></svg></div></figure>
<p>Your real prod bucket already has state like this. On a platform, <em>every
tenant's</em> bucket already has it. The seeding just gives you something to run
against out of the box.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-fork-prod-and-replay-the-load-bearing-call">3. Fork prod and replay (the load-bearing call)<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#3-fork-prod-and-replay-the-load-bearing-call" class="hash-link" aria-label="Direct link to 3. Fork prod and replay (the load-bearing call)" title="Direct link to 3. Fork prod and replay (the load-bearing call)" translate="no">​</a></h3>
<p>This is the heart of it: each variant gets its own zero-copy fork of prod, runs
every real thread's probe inside that fork so each answer is produced with the
customer's actual history in context, and the fork is dropped when we're done.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">run_variant_on_fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">prod</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> system_prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">*</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> run_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> keep</span><span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    fork </span><span class="token operator">=</span><span class="token plain"> prod</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">prod</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token string-interpolation interpolation">bucket</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">-eval-</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">name</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">-</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">run_id</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    graph </span><span class="token operator">=</span><span class="token plain"> build_agent</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">system_prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">compile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">checkpointer</span><span class="token operator">=</span><span class="token plain">fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    responses </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> t </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        out </span><span class="token operator">=</span><span class="token plain"> graph</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">invoke</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"messages"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"role"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"user"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"content"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">probe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"configurable"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"thread_id"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        responses</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> out</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"messages"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">content</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> keep</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        drop_bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">prod</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># erase the branch, no residue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> responses</span><br></div></code></pre></div></div>
<p>What makes this safe comes down to two properties.</p>
<p>First, the fork inherits <em>all</em> of prod: every thread, checkpoint, and learned
fact. Replaying conversations against it runs them against the real world, not a
mock that won't reflect it.</p>
<p>Second, the fork is a separate copy. The candidate's responses are written into
the fork and never into prod, so nothing the eval does can touch live data.</p>
<p>Together that lets you iterate on live state without fear. To evaluate ten
variants instead of two, call this in a loop; each fork is independent and costs
storage only where it diverges.</p>
<figure class="mermaidFrame_PGZu" style="max-width:38rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="600" height="216" style="width:100%;height:auto" viewBox="0 0 600 216" role="img" aria-label="Three rows: prod, baseline fork, and candidate fork. All three share the same wide block of checkpoints by reference. Prod stores no new writes during the eval because it is the source. Each fork stores only a small block of new writes, its own replies. A fork is a pointer, not a copy, so forking is O(1) whether prod holds 1 MB or 1 TB."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.small,.sub{font-size:12px;fill:#94a3b8}.small{fill:#bac1be}.shared,.writes{rx:4;ry:4;stroke-width:1}.shared{fill:#1a2e35;stroke:#2a3731}.writes{fill:rgba(98,254,181,.14);stroke:rgba(98,254,181,.45)}</style><text x="300" y="28" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    A fork is a pointer, not a copy
  </text><text x="293" y="60" class="sub" text-anchor="middle">
    shared checkpoints (every thread)
  </text><text x="525" y="60" fill="#62feb5" class="sub" text-anchor="middle">
    new writes
  </text><path stroke="#2a3731" stroke-dasharray="3 3" d="M488 68v116"></path><text x="98" y="92" class="small" text-anchor="end">prod</text><path d="M108 74h372v28H108z" class="shared"></path><text x="525" y="92" class="sub" text-anchor="middle">none (source)</text><text x="98" y="134" class="small" text-anchor="end">baseline fork</text><path d="M108 116h372v28H108z" class="shared"></path><path d="M494 116h34v28h-34z" class="writes"></path><text x="98" y="176" class="small" text-anchor="end">candidate fork</text><path d="M108 158h372v28H108z" class="shared"></path><path d="M494 158h28v28h-28z" class="writes"></path><text x="300" y="206" class="sub" text-anchor="middle">
    fork() is O(1): same speed whether prod holds 1 MB or 1 TB
  </text></svg></div></figure>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-judge-head-to-head-and-cancel-the-judges-bias">4. Judge head-to-head, and cancel the judge's bias<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#4-judge-head-to-head-and-cancel-the-judges-bias" class="hash-link" aria-label="Direct link to 4. Judge head-to-head, and cancel the judge's bias" title="Direct link to 4. Judge head-to-head, and cancel the judge's bias" translate="no">​</a></h3>
<p>Pairwise LLM-judging is how most teams actually grade agents, and it needs no
answer key. The one trap is position bias: judges tend to favor whichever reply
came first. So we ask twice with the order swapped and only count a win when it
is consistent across both.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">judge_report</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> baseline</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> candidate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> judge</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    results </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> t </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> c </span><span class="token operator">=</span><span class="token plain"> baseline</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> candidate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        v1 </span><span class="token operator">=</span><span class="token plain"> judge</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">probe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> c</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># A=baseline, B=candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        v2 </span><span class="token operator">=</span><span class="token plain"> judge</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">probe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> c</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># swapped</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> v1 </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"B"</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">and</span><span class="token plain"> v2 </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"A"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            winner </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"candidate"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">elif</span><span class="token plain"> v1 </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"A"</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">and</span><span class="token plain"> v2 </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"B"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            winner </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"baseline"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">else</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            winner </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"tie"</span><span class="token plain">          </span><span class="token comment" style="color:rgb(98, 114, 164)"># inconsistent: position bias, call it a tie</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        results</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">thread_id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> winner</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        recalled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">recall_markers</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        recalled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">c</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> t</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">recall_markers</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> results</span><br></div></code></pre></div></div>
<p>Alongside the verdict we run the cheap recall check from step 2: did each reply
contain the fact we know lives in that thread's history? Against fixtures that
number is <code>0/0</code> for both variants and the whole comparison is a wash. Against
real state, the gap is the regression your fixtures were hiding.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-run-it">5. Run it<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#5-run-it" class="hash-link" aria-label="Direct link to 5. Run it" title="Direct link to 5. Run it" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">thread       winner      baseline recall   candidate recall</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">------------ ----------- ----------------- ----------------</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cust-ana     candidate   no                yes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cust-ben     candidate   no                yes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cust-cleo    tie         yes               yes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cust-dan     candidate   no                yes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">head-to-head: candidate 3 / baseline 0 / ties 1</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">used real memory: candidate 4/4  vs  baseline 1/4</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">VERDICT: SHIP — candidate beats baseline on real state</span><br></div></code></pre></div></div>
<p>Your exact numbers will vary, but the judge runs at temperature 0, so the
verdict is stable across re-runs. The same change, evaluated against three blank
fixtures, would have come back a tie, and you'd have shipped it never knowing
which way it went.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-only-works-on-object-storage">Why this only works on object storage<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#why-this-only-works-on-object-storage" class="hash-link" aria-label="Direct link to Why this only works on object storage" title="Direct link to Why this only works on object storage" translate="no">​</a></h2>
<p>You can't bolt instant whole-agent forking onto a relational checkpointer,
because the cost model is wrong. A database reads rows to copy them, so
branching costs <em>more</em> the more history you have. That's exactly backwards.
Tigris shares immutable blocks and layers new writes on top, so a fork is a
pointer, not a duplication, and dropping it leaves nothing behind.</p>
<p>The shape of the problem changes once you're running a fleet. A database puts
every tenant behind one primary and a bounded connection pool, so the thing you
scale is a bottleneck you have to manage. Give each agent its own bucket and the
bottleneck disappears: the requests are independent, the state scales to zero
when idle, and any single agent forks in constant time.</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="680" height="236" style="width:100%;height:auto" viewBox="0 0 680 236" role="img" aria-label="A fleet-scaling comparison. On the left, a relational checkpointer: many agents all funnel through one shared connection pool into a single primary database, a bottleneck shared by every tenant, with WHERE tenant_id on every query. On the right, object storage on Tigris: each agent talks to its own bucket with independent requests, no shared connection ceiling, state that scales to zero, and fork() on any one bucket in O(1)."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.head{font-size:13px;fill:#e2e8f0;font-weight:600}.db{fill:#15242b;stroke:#3a4a44;stroke-width:1}.flow,.flow-green{stroke-width:1.3;fill:none}.flow{stroke:#3a4a44}.flow-green{stroke:rgba(98,254,181,.55)}</style><defs><marker id="a1" markerHeight="8" markerWidth="8" orient="auto" refX="5" refY="3"><path fill="#3a4a44" d="m0 0 5 3-5 3Z"></path></marker><marker id="a2" markerHeight="8" markerWidth="8" orient="auto" refX="5" refY="3"><path fill="rgba(98,254,181,0.55)" d="m0 0 5 3-5 3Z"></path></marker><g id="robot"><path stroke="#3a4a44" stroke-width="1.2" d="M17 0v6"></path><circle cx="17" cy="2" r="2" fill="#62feb5"></circle><rect width="30" height="22" x="2" y="6" fill="#1a2e35" stroke="#2a3731" rx="5" ry="5"></rect><circle cx="11" cy="16" r="2.6" fill="#62feb5"></circle><circle cx="23" cy="16" r="2.6" fill="#62feb5"></circle><path stroke="#3a4a44" stroke-width="1.2" d="M11 23h12"></path></g><g id="bucket"><path fill="none" stroke="rgba(98,254,181,0.45)" stroke-width="1.2" d="M4 6a16 13 0 0 1 32 0"></path><path fill="rgba(98,254,181,0.10)" stroke="rgba(98,254,181,0.5)" d="m2 6 6 26h24l6-26Z"></path><ellipse cx="20" cy="6" fill="#16242a" stroke="rgba(98,254,181,0.5)" rx="18" ry="5"></ellipse></g></defs><text x="340" y="28" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">
    Scaling a fleet: one database vs a bucket per agent
  </text><path stroke="#2a3731" stroke-dasharray="3 3" d="M340 48v176"></path><text x="170" y="64" class="head" text-anchor="middle">
    Relational checkpointer
  </text><use x="63" y="74" href="#robot"></use><use x="123" y="74" href="#robot"></use><use x="183" y="74" href="#robot"></use><use x="243" y="74" href="#robot"></use><path marker-end="url(#a1)" d="m80 104 82 28" class="flow"></path><path marker-end="url(#a1)" d="m140 104 27 28" class="flow"></path><path marker-end="url(#a1)" d="m200 104-27 28" class="flow"></path><path marker-end="url(#a1)" d="m260 104-82 28" class="flow"></path><path d="M100 134h140v24H100z" style="rx:5px;ry:5px;fill:rgba(248,113,113,.12);stroke:rgba(248,113,113,.5);stroke-width:1"></path><text x="170" y="150" style="font-size:11px;fill:#f1a8a8" text-anchor="middle">
    connection pool
  </text><path marker-end="url(#a1)" d="M170 158v18" class="flow"></path><ellipse cx="170" cy="178" class="db" rx="46" ry="8"></ellipse><path d="M124 178h92v30h-92z" class="db"></path><ellipse cx="170" cy="208" class="db" rx="46" ry="8"></ellipse><text x="170" y="198" class="head" text-anchor="middle">DB</text><text x="510" y="64" class="head" text-anchor="middle">
    Object storage (Tigris)
  </text><use x="408" y="74" href="#robot"></use><use x="468" y="74" href="#robot"></use><use x="528" y="74" href="#robot"></use><use x="588" y="74" href="#robot"></use><path marker-end="url(#a2)" d="M425 104v68" class="flow-green"></path><path marker-end="url(#a2)" d="M485 104v68" class="flow-green"></path><path marker-end="url(#a2)" d="M545 104v68" class="flow-green"></path><path marker-end="url(#a2)" d="M605 104v68" class="flow-green"></path><use x="405" y="174" href="#bucket"></use><use x="465" y="174" href="#bucket"></use><use x="525" y="174" href="#bucket"></use><use x="585" y="174" href="#bucket"></use></svg></div></figure>
<p>The same storage gives a platform the rest of what it needs. Lined up against a
database, the differences are not small:</p>
<table><thead><tr><th>What a platform needs</th><th>Relational checkpointer</th><th>Object storage (Tigris)</th></tr></thead><tbody><tr><td>Isolation between tenants</td><td>a <code>WHERE tenant_id</code> on every query</td><td>a bucket boundary, enforced by IAM</td></tr><tr><td>State per tenant</td><td>a stateful service to run and patch</td><td>a bucket from an API call, scales to zero</td></tr><tr><td>Bursty fleet concurrency</td><td>a bounded connection pool to manage</td><td>independent requests, no connection ceiling</td></tr><tr><td>Global reads</td><td>one primary plus replicas to keep in sync</td><td>served near the compute, globally distributed</td></tr><tr><td>Branch the whole agent</td><td><code>pg_dump</code> / restore, scales with history</td><td><code>fork()</code>, O(1) by reference</td></tr></tbody></table>
<p>LangGraph already defines the checkpointer seam, and Tigris just fills it, so
none of your graph code changes. The first four rows make object storage the
right home for a platform's agent state. The last row makes that home a
capability you can resell: per-PR eval environments, "clone this agent," instant
rollback, all the same <code>fork()</code> wearing different hats.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-doesnt-do">What it doesn't do<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#what-it-doesnt-do" class="hash-link" aria-label="Direct link to What it doesn't do" title="Direct link to What it doesn't do" translate="no">​</a></h3>
<p>Use a <strong>Single-region</strong> or <strong>Multi-region</strong> bucket. Those give the strongly
consistent reads the saver relies on to find the latest checkpoint, where Global
and Dual-region buckets can hand back a stale one. Forking makes the <em>state</em>
free, not the <em>inference</em>: an N-variant sweep is N times the tokens. And the
verdict is best read as a confidence signal that informs the decision, rather
than a hard gate that blocks the merge on its own.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-short-version">The short version<a href="https://www.tigrisdata.com/blog/eval-agents-real-state/#the-short-version" class="hash-link" aria-label="Direct link to The short version" title="Direct link to The short version" translate="no">​</a></h2>
<p>If you're on LangGraph, swapping in the Tigris checkpointer is one <code>pip install</code>
and one connection string. You keep resume, time travel, and human-in-the-loop.
What you gain is the move databases make too expensive to bother with: cloning
your entire agent in constant time, with no data copy. That's what lets you keep
an agent in line as you change it, evaluating each tweak against the real thing
instead of three fixtures.</p>
<p><strong>The checkpointer:</strong>
<a href="https://pypi.org/project/langgraph-checkpoint-tigris/" target="_blank" rel="noopener noreferrer" class=""><code>langgraph-checkpoint-tigris</code></a>
on PyPI.</p>
<p><strong>The full runnable example,</strong> seeded so it runs end to end against your own
bucket:
<a href="https://github.com/tigrisdata/tigris-langgraph/tree/main/examples/eval-on-real-state" target="_blank" rel="noopener noreferrer" class=""><code>examples/eval-on-real-state</code></a>.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Eval against the real world, not fixtures</span><p>A LangGraph checkpointer on Tigris object storage. Fork your whole agent in one call, replay real threads through a change, and judge it before you ship.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://pypi.org/project/langgraph-checkpoint-tigris/" class="cta-link"><div>Get langgraph-checkpoint-tigris<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>agents</category>
            <category>langgraph</category>
        </item>
        <item>
            <title><![CDATA[I taught a bucket to speak git]]></title>
            <link>https://www.tigrisdata.com/blog/objgit/</link>
            <guid>https://www.tigrisdata.com/blog/objgit/</guid>
            <pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[objgit is a single-binary git server that stores repositories directly in
Tigris — no disk, no git binary, no database. To my shock and horror, it
worked.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-baf7c5edcab708543188e76e67d3a0ef.webp" class="hero-image" alt="A blue tiger adventurer stands on a stone temple ledge in a misty mountain valley, channeling a stream of glowing magic from one hand into a rune-covered stone doorway, with a golden temple glowing in the distance">
<p>What happens if I just point a git server at an object storage bucket?</p>
<p>Back when I was porting
<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/" target="_blank" rel="noopener noreferrer" class="">agent sandboxes to Go</a>, I
built everything on top of
<a href="https://pkg.go.dev/github.com/go-git/go-billy/v6" target="_blank" rel="noopener noreferrer" class="">billy</a>, a filesystem
abstraction for Go. The whole trick of the project was teaching a Tigris bucket
to act enough like a filesystem that a shell interpreter and its tools couldn’t
tell the difference. Billy was the key layer that made the entire façade fall
into place.</p>
<!-- -->
<p>After I had gotten things working, I learned that I’m using billy way outside
its normal usecase. It was originally made for
<a href="https://pkg.go.dev/github.com/go-git/go-git/v6" target="_blank" rel="noopener noreferrer" class="">go-git</a>, a pure-Go
implementation of git’s protocols and data formats. It doesn’t rely on the
<code>/usr/bin/git</code> binary existing at all. Every method on billy’s filesystem
interface exists purely because go-git needs it. This gave me a terrible idea: I
already have a bucket that can quack like a filesystem and go-git’s native
language is “filesystem”.</p>
<p>Can this Just Work™? Let's find out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="git-was-always-an-object-store">Git was always an object store<a href="https://www.tigrisdata.com/blog/objgit/#git-was-always-an-object-store" class="hash-link" aria-label="Direct link to Git was always an object store" title="Direct link to Git was always an object store" translate="no">​</a></h2>
<p>If you strip away the porcelain, a git repository is 4 basic things:</p>
<ul>
<li class="">Objects, or compressed blobs of data. Most of the objects in any individual
repository are files.</li>
<li class="">Trees, or objects that map to other objects. TL;DR: trees are folders.</li>
<li class="">Commits, or objects that point at one tree and their parent commit. This lets
you pin down which files belong to one logical change set.</li>
<li class="">Refs, branches and tags, they are tiny mutable pointers into the pile of
objects.</li>
</ul>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Until I started working on this I was under the impression that git stored only
the patches done to an empty folder and that was how it reconstructed the
history of your repository. It does not. It actually keeps track of the entire
files, which explains why big binary blobs fudge the tooling so much. The diff
mental model works fine for using git day to day; it’s just wrong at the storage
layer, which is the layer this post lives in.</p></div></div>
<p>For example, let’s say I just made a new git repository and committed a
README.md to it. The tree for the <code>.git</code> folder looks something like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$ tree .git</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">.git</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── COMMIT_EDITMSG</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── config</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── HEAD</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── index</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── objects</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   ├── 5e</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   │   └── b8151eb669aa4467b6dea2c4bce19183cd0b41</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   ├── 6a</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   │   └── 6a8ecfcae2632152486aca3d9150ef83dedd66</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   ├── f4</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   │   └── d2487a1c6d742c8037c0296ddf80625190bd80</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   ├── info</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">│   └── pack</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">└── refs</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    ├── heads</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    │   └── main</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    └── tags</span><br></div></code></pre></div></div>
<p>As you can see there are three objects. One of them is the commit
<code>5eb8151eb669aa4467b6dea2c4bce19183cd0b41</code>, the next is the tree, and the last
one is the README file. The <code>main</code> branch also points to that commit:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">$ cat .git/refs/heads/main</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">5eb8151eb669aa4467b6dea2c4bce19183cd0b41</span><br></div></code></pre></div></div>
<p>The cool part is that half of this is content-addressed. The content-addressed
bits <em>never change</em> once they’ve been committed. Git objects are a great fit for
Tigris’ internal model because they are append-only storage, just like
<a href="https://www.tigrisdata.com/blog/append-only-storage/" target="_blank" rel="noopener noreferrer" class="">the fundamental model Tigris is built upon</a>.
The things that do change often are the refs, which are updated to point to the
latest commit. These are <em>tiny</em> files though, which means that Tigris can handle
them with no effort required.</p>
<p>However, when we host git repositories on a server, we end up creating single
points of failure. Our git repos are hosted on single machines that can and will
break. The entire implementation relies on git objects being 1:1 correlated with
filesystem objects because everyone (even GitHub) shells out to the git binary
to actually store files. Hosting git repos becomes one of the most stateful
services in our stateless cloud-native environment.</p>
<p>Sure git is in-theory decentralized, but most of us have ended up using that to
put our git repositories in one big store that has questionable uptime
practices: GitHub. To be fair to hubbers, GitHub operates at a scale that none
of us can really think about. They’ve been pushing the limits since their
inception where they had to get Engine Yard to keep building them bigger servers
to handle the load. They have to do everything with a big mounted filesystem
because git’s tooling gives them no other option.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-travesty-of-horrors-beyond-human-comprehension">A travesty of horrors beyond human comprehension<a href="https://www.tigrisdata.com/blog/objgit/#a-travesty-of-horrors-beyond-human-comprehension" class="hash-link" aria-label="Direct link to A travesty of horrors beyond human comprehension" title="Direct link to A travesty of horrors beyond human comprehension" translate="no">​</a></h2>
<p>Now suppose this weirdness bothers you enough to do something about it. To build
a git server <strong>without</strong> storing everything in the local filesystem, you have to
speak git somehow, and the conventional options aren’t really all that great:</p>
<ul>
<li class="">If you shell out to the git binary, now your “library” is the argv of the git
process and your error handling is screen-scraping output. Internally, git
implements its functionality with a billionty subcommands rather than exposing
it all as a library. The codebase is held together by load-bearing calls to
<code>die()</code>, which kills the process.</li>
<li class="">If you link into git’s guts with <code>libgit</code>, you inherit the “when things go
bad, <code>die()</code>” behaviour and your app now suddenly starts crashing at random.
This is not good for uptime.</li>
<li class="">If you try to use <code>libgit2</code> (the rewrite-that’s-actually-a-library), you have
to reckon with the fact that it’s addled by the GPL (with a linking exception,
try explaining that to your lawyers), you have to eat the jump to C every time
you do anything with git (very often), development has stalled, the Go
bindings have been archived, and it still assumes a local filesystem despite
assurances it does not.</li>
</ul>
<p>It might sound hopeless, right? You may be able to use WebAssembly or something
to contain the madness (assuming you have a good way to implement
<code>fork()</code>/<code>exec()</code> or <code>posix_spawn()</code> or something similar), but what if there
was a pure Go library that could handle this all for us?</p>
<p>Enter <a href="https://pkg.go.dev/github.com/go-git/go-git/v6" target="_blank" rel="noopener noreferrer" class="">go-git</a>, a pure-go
implementation of the git protocol and internals from scratch. This doesn’t rely
on cgo or <code>/usr/bin/git</code> and it does not assume the repositories are stored in
the local filesystem. Its storage interface is written against billy, the exact
interface I’ve already taught to speak Tigris. I wanted a git server that was
just in a bucket and the pieces were sitting there and calling to me.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="oh-no-it-works">Oh no, it works<a href="https://www.tigrisdata.com/blog/objgit/#oh-no-it-works" class="hash-link" aria-label="Direct link to Oh no, it works" title="Direct link to Oh no, it works" translate="no">​</a></h2>
<p>So I hacked up <a href="https://github.com/tigrisdata/objgit" target="_blank" rel="noopener noreferrer" class="">objgit</a>, a git server
backed by object storage. The only filesystem call I had to add to get it
booting was <code>MkdirAll</code>. I wired up the
<a href="https://pkg.go.dev/github.com/go-git/go-git/v6/plumbing/transport" target="_blank" rel="noopener noreferrer" class=""><code>transport</code></a>
package to a socket to implement the plaintext <code>git</code> protocol, hooked it up to a
bucket, and pushed the repo I was currently working on.</p>
<p>To my absolute astonishment, it worked.</p>
<p>Git pushed, pulled, logged, blamed, tagged, the whole kit and kaboodle. I didn’t
have to implement git myself, I just committed an egregious amount of shoving a
square peg into a round hole until the peg went in.</p>
<p>In hindsight this makes an annoying amount of sense. A bare repo is those four
kinds of things on a filesystem; swap the filesystem for object storage and
everything else Just Working™ is perfectly logical. Git’s on-disk format <em>is</em>
its database schema and if you fake open/stat/rename convincingly enough the
entire façade keeps working because APIs are the lies we tell ourselves to make
us sleep at night.</p>
<p>After a lot of hacking, I ended up with a feature list kinda like this:</p>
<ul>
<li class="">Push and pull over three transports: HTTP, classic <code>git://</code>, and SSH</li>
<li class="">Repositories upserted on first push</li>
<li class="">Absolutely no effort put into authentication as this is an experiment and
authentication is annoying and complicated</li>
<li class="">Prometheus metrics so I could optimize the filesystem layer</li>
</ul>
<p>Everything comes out of one Go binary with no local state, even the generated
SSH keys are stored in the bucket. You can run this in a Kubernetes cluster with
only the mutable storage required being temporary files for an optimistic cache
when doing smart git clones.</p>
<p>The rest of this post is what it took to get from “oh no, it works” to something
close to usable.</p>
<p>Obligatory disclaimer (like the best things in life): this is an experiment. It
has not been tested thoroughly or vetted for correctness. If it breaks in half,
you get to keep both pieces. Please do not move your company’s monorepo onto
this and then email me when it catches fire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="that-one-posix-idiom-that-survived">That one POSIX idiom that survived<a href="https://www.tigrisdata.com/blog/objgit/#that-one-posix-idiom-that-survived" class="hash-link" aria-label="Direct link to That one POSIX idiom that survived" title="Direct link to That one POSIX idiom that survived" translate="no">​</a></h2>
<p>Git is paranoid about durability, and its entire strategy is one Unix idiom that
you end up seeing many places: write new data to a temporary file and then
<code>rename(2)</code> it into place after you’ve assured it’s correct. POSIX guarantees
that rename is atomic, so readers either see the old file or the new one, not an
intermediate state inbetwixt the two. Packfiles (bundles of objects) land as
temporary files when uploaded then moved to their permanent home. Refs are
written as locked temporary files and then renamed over the ref. It’s rename all
the way down.</p>
<p>Object storage traditionally does not have rename as one atomic operation. S3’s
answer is to create exactly that intermediate state: <code>CopyObject</code> to the new
place and <code>DeleteObject</code> on the old one. This makes the most load-bearing idiom
in Git’s philosophy fall to pieces.</p>
<p>Luckily, Tigris has an extension for this:
<a href="https://www.tigrisdata.com/docs/objects/object-rename" target="_blank" rel="noopener noreferrer" class=""><code>RenameObject</code></a>. To use
it, pass an additional <code>X-Tigris-Rename: true</code> header to a <code>CopyObject</code> call and
instead of copying then deleting on the client, it moves the metadata around on
the server. One round trip, no data movement, and the Unix idiom maps on the
bucket 1:1. Objgit’s implementation of <code>Rename</code> is trivial:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)">// internal/s3fs/basic.go</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)">// RenameObject is a Tigris extension that renames in place (no data copy),</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)">// so we don't need a separate CopyObject + DeleteObject.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">copySource </span><span class="token operator">:=</span><span class="token plain"> fs3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">bucket </span><span class="token operator">+</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"/"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> src</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> fs3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">RenameObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CopyObjectInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">     </span><span class="token operator">&amp;</span><span class="token plain">fs3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    CopySource</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">copySource</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    Key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">        </span><span class="token operator">&amp;</span><span class="token plain">dst</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>A second, sneakier violation hides in the same codepath. When go-git writes a
temporary file, it creates that temporary file and then <strong>immediately</strong> starts
opening it for reading so it can build the pack index. You cannot do that with a
single live object in any object storage system, you are either reading or
writing, never both. I ended up working around this by cheating a bit and
buffering the contents of newly written pack files into memory so that this game
of chicken kept working. I may have to change this to write that pack cache to
the filesystem as trying to push <code>gcc.git</code> made me run out of RAM. At the very
least, everything lies consistently enough that git doesn’t care, so win!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="death-by-a-thousand-stat-calls">Death by a thousand <code>stat()</code> calls<a href="https://www.tigrisdata.com/blog/objgit/#death-by-a-thousand-stat-calls" class="hash-link" aria-label="Direct link to death-by-a-thousand-stat-calls" title="Direct link to death-by-a-thousand-stat-calls" translate="no">​</a></h2>
<p>With this correctness sorted, I tried pushing the
<a href="https://github.com/golang/go" target="_blank" rel="noopener noreferrer" class="">golang/go</a> repository to objgit to see how long
it would take. It did work, but it took <em>forever</em>. Using the prometheus metrics
I mentioned before, I saw that it was making biblical amounts of <code>HeadObject</code>
calls. Some blocking profile analysis pointed to the fact that the git library
was using the <code>stat()</code> call to detect if a file exists. The flow was like:</p>
<ul>
<li class="">Client has object x</li>
<li class="">Check if object x exists</li>
<li class="">Check if any pack has object x</li>
</ul>
<p>And so on ad infinitum. This is fine-ish on a local filesystem because those
syscalls resolve in <em>microseconds</em>, not the tens of milliseconds it takes to get
from my office to the nearest Tigris region (please expand to Ottawa, I would
love that so much).</p>
<p>This was compounded with a discovery that the transport I was using (SSH —
classic <code>git://</code> shares the same code path) was exploding every packfile into
<em>loose objects</em> when pushing it. Each loose object write was costing two round
trips: <code>stat()</code> to check if a file exists and then <code>open()</code> / <code>write()</code> to
actually put the data into Tigris. This made a 100,000 object packfile cost
200,000 object storage calls. Call it 10ms of latency for each one, and that’s
over half an hour of waiting for responses that mostly say “404 not found”.</p>
<p>Caching can’t really save you here either, read caches would absorb the repeated
reads; but this is a firehose of <em>writes</em> to 100,000 paths that probably have
never been read and likely will never be seen again.</p>
<p>The reason only two transports had this problem is a deadlock story. The git
library's fast path stores an incoming pack whole through its <code>PackfileWriter</code>,
by copying from the connection until <code>io.EOF</code>. Over HTTP that's fine: the
request body ends, EOF arrives, everyone goes home. Over <code>git://</code> and SSH, the
connection is a persistent socket and the client is holding it open, politely
waiting for the server's status report. EOF never comes. The copy waits forever,
the client waits forever, and you have invented a distributed deadlock with two
participants. The original workaround was to hide the <code>PackfileWriter</code>
capability on those transports so go-git fell back to its streaming parser that
writes every object loose. Hence the stat storm.</p>
<p>So the solution was to stop depending on EOF at all. Packfiles are
self-delimiting: the header says how many objects are coming and a trailing
checksum marks the end, so a packfile scanner walks the stream and stops at the
trailer while a <code>TeeReader</code> mirrors exactly those bytes into the
<code>PackfileWriter</code>. This makes the rest of the façade fall into place and the git
library is happy. This made pushes into two uploads: a packfile and its index
instead of a torrent of round trips that mean nothing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-about-cloning">What about cloning?<a href="https://www.tigrisdata.com/blog/objgit/#what-about-cloning" class="hash-link" aria-label="Direct link to What about cloning?" title="Direct link to What about cloning?" translate="no">​</a></h2>
<p>Once I got pushing fixed, I moved on to the read path. In order to emulate
<code>ReadAt</code>, I used ranged <code>GetObject</code> requests so that the git library could read
individual objects out of packfiles. I was happy with this hack, but there was
one problem: the latency curse struck again. Cloning a simple repo with 318
objects and a 200KiB packfile made over 8,500 <code>GetObject</code> calls before I killed
it.</p>
<p>A git client cloning a repository reads repository packfiles thousands of times
with random access, walking objects and candidate delta bases over and over. On
a local disk you never notice because your page cache eats that for breakfast.
When every call is an HTTP request, a 200KiB repo turns into dozens of megabytes
of round trips. A 20MiB repo was effectively unservable.</p>
<p>In other words, I had un-cached the one workload that caching was designed to
solve.</p>
<p>The fix leans on a gift from git: pack files are immutable and
content-addressed. <code>pack-&lt;sha&gt;.pack</code> will <em>never</em> change for as long as it
exists. This makes them trivially cacheable to a faster local medium, such as
the filesystem. No invalidation logic is required. I made objgit download packs
to a local temporary folder and serve reads from there. To be on the safe side,
I did add least-recently-used caching to the mix so that my temp folder wouldn’t
blow up unexpectedly. This does mean that the first request for pack files is
slower, but then everything else is at filesystem speed.</p>
<p>Yes, this relies on the local disk again, but only as a cache that can and will
be thrown away. I think trading a stateless ideal for clones that terminate in
reasonable amounts of time is a worthwhile bargain.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-so-listobjectsv2-batman">Why so <code>ListObjectsV2</code>, Batman?<a href="https://www.tigrisdata.com/blog/objgit/#why-so-listobjectsv2-batman" class="hash-link" aria-label="Direct link to why-so-listobjectsv2-batman" title="Direct link to why-so-listobjectsv2-batman" translate="no">​</a></h2>
<p>Once the other disasters were out of the way, one more remained: the metrics
showed a flood of <code>ListObjectsV2</code> calls every time a clone was made and didn’t
stop making those calls after it was done.</p>
<p>Two things compounded. First, when git looks up an object that isn't packed, it
probes for a loose object at <code>objects/&lt;xx&gt;/&lt;rest-of-hash&gt;</code>. objgit keeps packs
whole, so there are no loose objects, so every probe misses, and each miss
across a distinct two-hex prefix triggered a directory listing to find out.
There are 256 possible prefixes. A single clone could issue up to 256
<code>ListObjectsV2</code> calls whose collective answer was a resounding "there is nothing
here."</p>
<p>Second (and more embarrassing), the listing cache already had an optimization
for this. It collapsed entire subtree lookups into recursive scans so a single
listing of the repository could answer every stat() and probe beneath it. It was
completely dead in production. The cache matched recursive prefixes against the
repo root (<code>refs/</code>), but every repo is chrooted to its own directory, so real
keys look like <code>myrepo.git/refs/heads/main</code>. The prefix check wasn’t aware of
chroots so it never actually matched anything. Nobody noticed because a cache
that degrades to “no caching” still returns the correct answer, just slowly. To
rub it in, a cache warmer was dutifully re-listing every one of those useless
prefixes every 30 seconds for 10 minutes after each clone. Thousands of
background list calls were burned in the service of caching nothing of use.</p>
<p>The fix was insultingly small: when a repo’s filesystem gets chrooted, register
that chroot as a recursive subtree root within the cache. This made the cache
actually useful and resulted in only one <code>ListObjectsV2</code> call instead of
hundreds. Every sufficiently advanced cache is indistinguishable from a no-op
until someone graphs the miss rate.</p>
<p>None of these disasters were exotic. They’re the things filesystems and kernels
give you for free — and every perfectly reasonable disk assumption fell to
pieces once a network round trip sat at the core. Serving Git repositories is an
accidental filesystem latency benchmark. If your storage abstraction has a weak
point, Git <em>will</em> find it and the metrics will show you where that problem is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="post-receive-hooks-go-in-clown-jail">Post-receive hooks go in clown jail<a href="https://www.tigrisdata.com/blog/objgit/#post-receive-hooks-go-in-clown-jail" class="hash-link" aria-label="Direct link to Post-receive hooks go in clown jail" title="Direct link to Post-receive hooks go in clown jail" translate="no">​</a></h2>
<p>One of the most useful parts of hosting your own git server is setting up
post-receive hooks. These have been used since time immemorial for things like
<a href="https://gist.github.com/nonbeing/f3441c96d8577a734fa240039b7113db" target="_blank" rel="noopener noreferrer" class="">automatic deployments</a>
when you push code to the server. The heart of this is how we get systems like
GitHub Actions: it’s code that runs when you are done pushing.</p>
<p>When you push to objgit with <code>--allow-hooks</code> enabled, it looks for a
post-receive hook in <code>.objgit/hooks/receive-pack</code> (this corresponds to the git
plumbing action, the name can and will be changed) in the tree of the commit you
just pushed. It will then spin up a
<a href="https://xeiaso.net/blog/2026/dancing-mad-sandboxing/" target="_blank" rel="noopener noreferrer" class="">kefka</a> sandbox with a
checkout of the git repository at the commit you just pushed mounted at <code>/src</code>
and mutable temporary files at <code>/tmp</code>. It gets coreutils and nothing else. No
host filesystem, no network, no arbitrary binaries. Output streams back into the
pusher as <code>remote:</code> lines just like when you <code>git push heroku main</code>. Eventually
I want to make custom commands to allow you to deploy
<a href="https://tekton.dev/" target="_blank" rel="noopener noreferrer" class="">Tekton</a> pipeline changes and kick off CI jobs that way,
but for now I’m happy with this working at all.</p>
<p>You can’t implement policy using these hooks yet. I’m working on it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="now-what">Now what?<a href="https://www.tigrisdata.com/blog/objgit/#now-what" class="hash-link" aria-label="Direct link to Now what?" title="Direct link to Now what?" translate="no">​</a></h2>
<p>I taught a bucket to speak git. Where this goes next, roughly in order of how
much the ideas keep me up at night.</p>
<p>CI is the obvious next step. I would wire up commands for things like “apply
kubernetes object” and “create tekton pipeline run” so that CI would run via
your friendly neighborhood Kubernetes cluster and then notify you through some
reasonable mechanism. That’s the first thing I’ll build when I have the time.</p>
<p>It would be nice to have a web UI for this, which is complicated for reasons
that have <em>nothing</em> to do with git trees, object storage, or anything else and
everything to do with the current state of the internet. Git lookups are
expensive in the best cases and with the current torrent of unethical scraping
ransacking git servers for every scrap of RAM they have, it’s probably a bad
idea to implement this without a lot of clever optimizations. Maybe the fact
that this doesn’t have load-bearing dependencies on <code>/usr/bin/git</code> would make it
more resilient against scrapers. The fact that this is based on object storage
could also mean that caching would be a bit easier (having basically unlimited
storage is kind of a low-key superpower for caching), but then the main issue
would be server load. It’s a tough cookie to handle.</p>
<p>Performance and stability are another place this needs to improve. I’ve tested
this on my developer workstation but that is far different from testing it in
production. There’s some other performance issues that are easy to fix, but the
big one is latency to Tigris. Maybe I can get the devops team to set me up a
<a href="https://rancher.github.io/k3k-product-docs/k3k/latest/en/introduction.html" target="_blank" rel="noopener noreferrer" class="">k3k cluster</a>
in production.</p>
<p>Right now this is an experiment as I plug along and feel out the shape of what
git-on-object-storage can be. A git server with no disk, no git binary, and no
database. If you want to take a look,
<a href="https://github.com/tigrisdata/objgit" target="_blank" rel="noopener noreferrer" class="">check it out on GitHub</a>.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Teach your own bucket to speak git</span><p>objgit is a single-binary git server that stores repositories directly in Tigris — no disk, no git binary, no database. Point it at a bucket and push.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://github.com/tigrisdata/objgit" class="cta-link"><div>Check out objgit on GitHub<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>build with tigris</category>
            <category>engineering</category>
            <category>go</category>
        </item>
        <item>
            <title><![CDATA[Tar saved Unix backups in 1979. Now it saves your dataloader.]]></title>
            <link>https://www.tigrisdata.com/blog/bundle-api/</link>
            <guid>https://www.tigrisdata.com/blog/bundle-api/</guid>
            <pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Tigris bundles let you pull thousands of objects in one HTTP request as a streaming tar archive — no more one GET per object. Here's how it works and why.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-2423b5f25617b796eba9d51333a6c418.webp" class="hero-image" alt="A blue-striped tiger in a postal worker's jacket stands in a vintage post office, stuffing envelopes, memos, photos, a VHS tape, and polyhedral 3D models into a large canvas sack labeled BUNDLE">
<p>Back in 1979, the people building Unix had a very physical problem: how do you
get a directory full of little files onto a magnetic tape so you can back it up?
Tape is sequential. You can't seek around on it like you can a disk. So they
invented a format that streams every file's metadata and contents back to back
into one continuous blob: the tape archive, or <code>tar</code>. A tarball is just a header
describing a file, then that file's bytes, then the next header, then the next
file's bytes, all the way down until you hit the end.</p>
<p>Fifty-some years later I keep running into the exact same problem, except the
tape is an object storage bucket and the files are training samples. You've got
a few million tiny objects sitting in a bucket, and you need to pull thousands
of them at a time, fast. The old solution turns out to be the new solution.
Tigris now lets you grab a whole pile of objects in one request with
<a href="https://www.tigrisdata.com/docs/objects/bundle/" target="_blank" rel="noopener noreferrer" class="">bundles</a>, and the thing it
hands back is a tar stream.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-lots-of-little-files">The problem: lots of little files<a href="https://www.tigrisdata.com/blog/bundle-api/#the-problem-lots-of-little-files" class="hash-link" aria-label="Direct link to The problem: lots of little files" title="Direct link to The problem: lots of little files" translate="no">​</a></h2>
<p>When you're assembling a dataset, you almost always end up with a smattering of
small objects. Images get ingested one at a time as they're discovered. Audio
clips, JSON samples, parquet shards, whatever it is; they land in your bucket as
individual keys because that's how they showed up. You don't get to pick.</p>
<p>Training is the opposite shape of work. A dataloader wants to pull a batch of a
few thousand samples, do it again for the next batch, and keep your GPUs fed so
they're not sitting idle burning money. The access pattern wants big sequential
reads. The data is stored as tiny scattered ones.</p>
<p>Most object storage makes you reconcile that mismatch the hard way: one <code>GET</code>
per object. If your batch is 4,000 images, that's 4,000 separate HTTP requests,
each with its own request line, its own headers, its own round trip to the
server and back. Even if your client is smart enough to reuse a connection and
fire requests concurrently (or lucky enough to get HTTP/2 multiplexing), you're
still paying per-object request overhead thousands of times per batch.</p>
<p>Here's the napkin math. Say a round trip to the bucket is 30 ms. Do those 4,000
<code>GET</code>s strictly one after another and you've spent 120 seconds just waiting on
latency, before counting a single byte of actual image data. Crank concurrency
up to 64 in flight and you're down to roughly 1.9 seconds of pure latency
overhead per batch. That's better, but it's 1.9 seconds your GPUs spent doing
nothing, every batch, forever.</p>
<p>A bundle collapses all of that into one request. One round trip, one response,
one stream of bytes that contains every object you asked for.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-works">How it works<a href="https://www.tigrisdata.com/blog/bundle-api/#how-it-works" class="hash-link" aria-label="Direct link to How it works" title="Direct link to How it works" translate="no">​</a></h2>
<p>When you request a bundle, you send a list of object keys to Tigris and the
format you want to get the bundle in. The server starts writing the archive
directly to you, then walks down your list of keys and appends each object on
the end as it goes. Tigris never buffers the whole archive server-side, so the
first bytes reach you while it's still pulling the last objects off disk.</p>
<p>Your client can read that and then unpack it with any tar library such as
<a href="https://pkg.go.dev/archive/tar" target="_blank" rel="noopener noreferrer" class="">the one in Go's standard library</a>,
<a href="https://docs.python.org/3/library/tarfile.html" target="_blank" rel="noopener noreferrer" class="">Python's stdlib <code>tarfile</code> library</a>,
or <a href="https://www.npmjs.com/package/tar-stream" target="_blank" rel="noopener noreferrer" class=""><code>tar-stream</code> in JavaScript</a>. Worst
comes to worst, you can write it to a file and shell out to <code>tar</code> by hand, or
pipe the HTTP response straight into <code>tar</code>'s standard input — whatever you (or
your agent) can write the code for.</p>
<p>Nothing exciting is happening here, which is the point: it's just another
authenticated request, and the only permission you need is <code>GetObject</code>. You can
even do it with <code>curl</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-youd-actually-want-this">Why you'd actually want this<a href="https://www.tigrisdata.com/blog/bundle-api/#why-youd-actually-want-this" class="hash-link" aria-label="Direct link to Why you'd actually want this" title="Direct link to Why you'd actually want this" translate="no">​</a></h2>
<p>Feeding training batches is the obvious win, the case we just did the math on,
and the one bundles were built for. You need thousands of samples per batch
without eating thousands of round trips, and that's where the latency savings
are the most dramatic.</p>
<p>It's not the only place this shows up, though. A few others:</p>
<ul>
<li class=""><strong>GDPR export requests.</strong> Someone exercises their right to get a copy of their
data. Their data is spread across a few hundred objects in your bucket.
Instead of orchestrating hundreds of downloads and stitching them together,
you hand the server one list of keys and get back a single tarball you can
stream straight to the user. Their whole pile, one file.</li>
<li class=""><strong>Shipping game assets.</strong> Games are made of seemingly infinite numbers of tiny
files. In an MMO with customizable armor across, say, five playable races,
adding a single shirt to the game can mean shipping 20-plus different 3D
models, textures, and material definitions just to make that one shirt render
correctly on everybody. Pulling those as a bundle beats pulling them one at a
time while a player stares at a loading bar.</li>
</ul>
<p>Anywhere you know the exact set of objects you need up front and you need them
together, a bundle turns N requests into one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="using-it-from-the-sdks">Using it from the SDKs<a href="https://www.tigrisdata.com/blog/bundle-api/#using-it-from-the-sdks" class="hash-link" aria-label="Direct link to Using it from the SDKs" title="Direct link to Using it from the SDKs" translate="no">​</a></h2>
<p>Every SDK wraps the same flow: make the request, get a stream back, and read
members straight out of it without ever touching disk. Here's that call in each
language:</p>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">cURL</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">CLI</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Python</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Go</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">JavaScript</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"><p>Pipe the response straight into <code>tar</code> to list the contents:</p><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">curl -X POST "https://t3.storage.dev/my-bucket?bundle" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --aws-sigv4 "aws:amz:auto:s3" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --user "$TIGRIS_STORAGE_ACCESS_KEY_ID:$TIGRIS_STORAGE_SECRET_ACCESS_KEY" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  -H "x-tigris-bundle-format: tar" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  -d '{"keys":["dataset/train/img_00001.jpg","dataset/train/img_00002.jpg"]}' \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  | tar -tv</span><br></div></code></pre></div></div><p>Pass <code>-x</code> to extract them:</p><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">curl -X POST "https://t3.storage.dev/my-bucket?bundle" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --aws-sigv4 "aws:amz:auto:s3" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --user "$TIGRIS_STORAGE_ACCESS_KEY_ID:$TIGRIS_STORAGE_SECRET_ACCESS_KEY" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  -H "x-tigris-bundle-format: tar" \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  -d '{"keys":["dataset/train/img_00001.jpg","dataset/train/img_00002.jpg"]}' \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  | tar -x</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><p>The <a href="https://www.tigrisdata.com/docs/cli/bundle/" target="_blank" rel="noopener noreferrer" class=""><code>tigris</code> CLI</a> wraps the same
request. Point it at a bucket, hand it a list of keys, and it writes the tar
stream wherever you want:</p><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris bundle my-bucket \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --keys dataset/train/img_00001.jpg,dataset/train/img_00002.jpg \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --output batch.tar</span><br></div></code></pre></div></div><p>The keys can come from a file (one per line) or stdin instead of the flag, and
you can compress the stream on the way out with <code>--compression gzip</code> (or
<code>zstd</code>):</p><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">cat keys.txt | tigris bundle t3://my-bucket --compression gzip -o batch.tar.gz</span><br></div></code></pre></div></div><p>Pass <code>--on-error fail</code> to make a missing key abort the whole bundle instead of
getting skipped and logged in <code>__bundle_errors.json</code>.</p></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><p>Install the boto3 extension with <code>pip install tigris-boto3-ext</code>:</p><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> tarfile</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> tigris_boto3_ext </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> bundle_objects</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">response </span><span class="token operator">=</span><span class="token plain"> bundle_objects</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">s3_client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_001.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_002.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> tarfile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">fileobj</span><span class="token operator">=</span><span class="token plain">response</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> mode</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"r|"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> member </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> member</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">name </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"__bundle_errors.json"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token comment" style="color:rgb(98, 114, 164)"># Handle any errors here</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">continue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        f </span><span class="token operator">=</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">extractfile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">member</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> f </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">is</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> </span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            image_bytes </span><span class="token operator">=</span><span class="token plain"> f</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">read</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token comment" style="color:rgb(98, 114, 164)"># do something with image_bytes</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><p>Install the SDK with <code>go get github.com/tigrisdata/storage-go</code>:</p><div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">output</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">BundleObjects</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">BundleObjectsInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    Keys</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_001.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_002.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Fatal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">defer</span><span class="token plain"> output</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Close</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tr </span><span class="token operator">:=</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">NewReader</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">output</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    hdr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> tr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Next</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">==</span><span class="token plain"> io</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">EOF </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">break</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Fatal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> hdr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Name </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"__bundle_errors.json"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)">// handle any errors here</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">continue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token boolean">_</span><span class="token plain"> </span><span class="token operator">:=</span><span class="token plain"> io</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ReadAll</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">tr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// process hdr.Name, data</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><p>Install the SDK with <code>npm install @tigrisdata/storage tar-stream</code>:</p><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token imports"> bundle </span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@tigrisdata/storage/server"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports">tar</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"tar-stream"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> result </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">bundle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_001.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"dataset/train/img_002.jpg"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">result</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">error</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">throw</span><span class="token plain"> result</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">error</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> extract </span><span class="token operator">=</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">extract</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">extract</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"entry"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token parameter">header</span><span class="token parameter punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token parameter"> stream</span><span class="token parameter punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token parameter"> next</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">header</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">name</span><span class="token plain"> </span><span class="token operator">===</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"__bundle_errors.json"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// handle any errors here</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    stream</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">resume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token function" style="color:rgb(80, 250, 123)">next</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> chunks </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  stream</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"data"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token parameter">chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> chunks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">push</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">chunk</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  stream</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"end"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token arrow operator">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> data </span><span class="token operator">=</span><span class="token plain"> </span><span class="token maybe-class-name">Buffer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">concat</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">chunks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">${</span><span class="token template-string interpolation">header</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">name</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token template-string string" style="color:rgb(255, 121, 198)">: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">${</span><span class="token template-string interpolation">data</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">length</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token template-string string" style="color:rgb(255, 121, 198)"> bytes</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token function" style="color:rgb(80, 250, 123)">next</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  stream</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">resume</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token maybe-class-name">Readable</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"stream"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token maybe-class-name">Readable</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">fromWeb</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">result</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">pipe</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">extract</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div></div></div>
<p>Since the dataloader case is the whole reason this exists, here's what it looks
like wired into a PyTorch <code>IterableDataset</code> that prefetches a chunk of batches
per bundle request:</p>
<details style="max-width:50rem;margin-left:auto;margin-right:auto" class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>PyTorch dataloader example</summary><div><div class="collapsibleContent_i85q"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> random</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> tarfile</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> io </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> BytesIO</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> torch</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> PIL </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> Image</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> tigris_boto3_ext </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> bundle_objects</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">build_batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">metadata_path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> batch_size</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">"""Load object keys from a metadata file and split them into batches."""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">TigrisBundleDataset</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">torch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">utils</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">IterableDataset</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">__init__</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> s3_client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> metadata_path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> batch_size</span><span class="token operator">=</span><span class="token number">32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> prefetch</span><span class="token operator">=</span><span class="token number">20</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">s3_client </span><span class="token operator">=</span><span class="token plain"> s3_client</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">bucket </span><span class="token operator">=</span><span class="token plain"> bucket</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">batch_size </span><span class="token operator">=</span><span class="token plain"> batch_size</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">prefetch </span><span class="token operator">=</span><span class="token plain"> prefetch</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">batches </span><span class="token operator">=</span><span class="token plain"> build_batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">metadata_path</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> batch_size</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">__iter__</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        worker_info </span><span class="token operator">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">utils</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">data</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">get_worker_info</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> worker_info </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">is</span><span class="token plain"> </span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            my_batches </span><span class="token operator">=</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">batches</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">else</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            my_batches </span><span class="token operator">=</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">worker_info</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">worker_info</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">num_workers</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        random</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">shuffle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">my_batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> i </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">range</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">len</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">my_batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">prefetch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            chunk </span><span class="token operator">=</span><span class="token plain"> my_batches</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">i </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> i </span><span class="token operator">+</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">prefetch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            keys </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">row</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"key"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> batch </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> chunk </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> row </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> batch</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> bundle_objects</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">s3_client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> keys</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> response</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> tarfile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">fileobj</span><span class="token operator">=</span><span class="token plain">response</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> mode</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"r|"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> member </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> member</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">name </span><span class="token operator">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"__bundle_errors.json"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                            </span><span class="token comment" style="color:rgb(98, 114, 164)"># handle any errors here</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">continue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        f </span><span class="token operator">=</span><span class="token plain"> tar</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">extractfile</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">member</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> f </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">is</span><span class="token plain"> </span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                            </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">continue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        image </span><span class="token operator">=</span><span class="token plain"> Image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">open</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">BytesIO</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">f</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">read</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">convert</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"RGB"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">yield</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"image"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> image</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div><p>Swap the <code>Image.open</code> decode for whatever your samples are: <code>json.loads</code>,
<code>torch.load</code>, a parquet reader, whatever fits.</p></div></div></details>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-when-a-key-is-missing">What happens when a key is missing<a href="https://www.tigrisdata.com/blog/bundle-api/#what-happens-when-a-key-is-missing" class="hash-link" aria-label="Direct link to What happens when a key is missing" title="Direct link to What happens when a key is missing" translate="no">​</a></h2>
<p>Remember the <code>__bundle_errors.json</code> file every example skips? That's how missing
keys get reported.</p>
<p>By default, bundles run in <strong>skip</strong> mode: if a key doesn't exist, the server
leaves it out instead of failing the whole request, and lists what it dropped
(and why) in <code>__bundle_errors.json</code>. It's metadata, not one of your objects, so
read it if you care which keys went missing and skip it otherwise.</p>
<p>If you'd rather fail loudly, send <code>x-tigris-bundle-on-error: fail</code> (the SDKs
have an equivalent flag). The server then checks every key up front and returns
a <code>404</code> listing what's absent, instead of a partial tarball. Skip mode keeps you
training through a few gaps; fail mode is for when the bundle has to be
complete.</p>
<p>There are a handful of limits worth keeping in your back pocket: up to 5,000
keys per request, up to 50 GB assembled, a 5 MB cap on the request body itself,
and a 15-minute timeout on the whole thing. If you're pulling more than 5,000
objects, batch your batches.</p>
<p>One honest tradeoff: a bundle is a sequential stream, not a random-access
archive. You can't range-request a single member out of the middle of it, and
one enormous object in the list will stream in its entirety before you get to
the next one. For the "I know exactly which thousand small files I want"
workload that's exactly right. For "I want one specific chunk of an 8 GB file,"
a plain old <code>GET</code> is still the better tool.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-web-console-does-it-too">The web console does it too<a href="https://www.tigrisdata.com/blog/bundle-api/#the-web-console-does-it-too" class="hash-link" aria-label="Direct link to The web console does it too" title="Direct link to The web console does it too" translate="no">​</a></h2>
<p>Don't want to write any code? The web console downloads a bundle straight from
the object browser: select the objects you want, hit download, and you get back
the same tar stream the API hands you.</p>
<video controls="" autoplay="" loop="" muted="" playsinline="" width="100%"><source src="/blog/img/blog/bundle-api/bundle-demo.mp4" type="video/mp4"><p>Download the <a href="https://www.tigrisdata.com/blog/img/blog/bundle-api/bundle-demo.mp4">MP4</a> version.</p></video>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-forth-and-bundle">Go forth and bundle<a href="https://www.tigrisdata.com/blog/bundle-api/#go-forth-and-bundle" class="hash-link" aria-label="Direct link to Go forth and bundle" title="Direct link to Go forth and bundle" translate="no">​</a></h2>
<p>Object storage spent a long time pretending that the only thing you ever want is
one object at a time. The reality of how people actually use buckets, especially
for AI training, is that you constantly want a known set of objects together
right now without paying a latency tax per file. Borrowing the oldest trick in
the Unix book turns out to be a clean way to give that to you.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Feed your GPUs, not your latency budget</span><p>Pull thousands of objects in a single request as a streaming tar archive. Tigris bundles are built for dataloaders that can't afford to wait.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/objects/bundle/" class="cta-link"><div>Read the bundle docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>object storage</category>
            <category>machine learning</category>
            <category>s3</category>
        </item>
        <item>
            <title><![CDATA[Introducing Soft Delete for Tigris Buckets and Objects]]></title>
            <link>https://www.tigrisdata.com/blog/soft-delete/</link>
            <guid>https://www.tigrisdata.com/blog/soft-delete/</guid>
            <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Tigris now supports soft delete for buckets and objects. Enable it once, and every delete becomes recoverable for up to 90 days before it's permanently removed.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-0245d3f869b296937d90bcea85963fcf.webp" class="hero-image" alt="Ty the tiger braced on the broken edge of an ancient stone bridge, hauling a glowing golden relic out of a swirling vortex of dissolving artifacts with a luminous rope, the recovered relics resting on stone pedestals behind him and the Tigris kingdom in the misty distance">
<p>Today we're shipping
<a href="https://www.tigrisdata.com/docs/buckets/soft-delete/" target="_blank" rel="noopener noreferrer" class="">soft delete</a> for Tigris
buckets and objects. When you turn it on for a bucket, deletes stop being
permanent. They go into a holding state for a retention window you set, anywhere
from 7 to 90 days. Restore inside that window and the bucket or object comes
back. Wait the window out and Tigris removes the data on its own.</p>
<p>A wrong delete is no longer a one-way door. Work that used to need a staging
environment or a careful permissions audit can happen on the real bucket,
because if something goes wrong, you have a few days to fix it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deletes-go-into-a-holding-state">Deletes go into a holding state<a href="https://www.tigrisdata.com/blog/soft-delete/#deletes-go-into-a-holding-state" class="hash-link" aria-label="Direct link to Deletes go into a holding state" title="Direct link to Deletes go into a holding state" translate="no">​</a></h2>
<figure class="mermaidFrame_PGZu" style="max-width:44rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" style="background:0 0;width:100%;height:auto;max-width:100%;display:block" viewBox="0 0 820 340" role="img" aria-label="Soft delete lifecycle: a delete moves a Live bucket or object into a Soft-deleted state, where it stays recoverable for a retention window of 7 to 90 days. A restore returns it to Live; if the retention window expires, it moves to Permanently removed and the storage is reclaimed."><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.label{font-size:13px;fill:#e2e8f0;font-weight:600}.small{font-size:11px;fill:#bac1be}.box{rx:6;ry:6;fill:#1a2e35;stroke:#2a3731;stroke-width:1}</style><defs><marker id="arrowhead" markerHeight="6" markerWidth="6" orient="auto" refX="8" refY="5" viewBox="0 0 10 10"><path fill="#94a3b8" d="m0 0 10 5-10 5z"></path></marker><marker id="arrowhead-pass" markerHeight="6" markerWidth="6" orient="auto" refX="8" refY="5" viewBox="0 0 10 10"><path fill="#62feb5" d="m0 0 10 5-10 5z"></path></marker><marker id="arrowhead-fail" markerHeight="6" markerWidth="6" orient="auto" refX="8" refY="5" viewBox="0 0 10 10"><path fill="#fbbf24" d="m0 0 10 5-10 5z"></path></marker></defs><text x="410" y="34" style="font-size:16px;fill:#62feb5;font-weight:600" text-anchor="middle">Soft delete lifecycle</text><text x="410" y="56" style="font-size:12px;fill:#94a3b8" text-anchor="middle">every delete becomes recoverable for 7–90 days, then is permanently removed</text><path d="M40 170h160v70H40z" class="box"></path><text x="120" y="200" class="label" text-anchor="middle">Live</text><text x="120" y="220" class="small" text-anchor="middle">bucket or object</text><path d="M320 170h180v70H320z" class="box"></path><text x="410" y="200" class="label" text-anchor="middle">Soft-deleted</text><text x="410" y="220" class="small" text-anchor="middle">recoverable</text><path d="M620 170h160v70H620z" style="rx:6px;ry:6px;fill:rgba(251,191,36,.08);stroke:rgba(251,191,36,.45);stroke-width:1"></path><text x="700" y="200" class="label" text-anchor="middle">Permanently</text><text x="700" y="220" class="label" text-anchor="middle">removed</text><path marker-end="url(#arrowhead)" d="M200 205h118" style="stroke:#94a3b8;stroke-width:1.5;fill:none"></path><text x="259" y="195" style="font-size:11px;fill:#94a3b8;font-weight:500" text-anchor="middle">delete</text><path marker-end="url(#arrowhead-pass)" d="M360 170q-100-70-200 0" style="stroke:#62feb5;stroke-width:1.5;fill:none"></path><text x="260" y="124" style="font-size:11px;fill:#62feb5;font-weight:600" text-anchor="middle">restore</text><path marker-end="url(#arrowhead-fail)" d="M500 205h118" style="stroke:#fbbf24;stroke-width:1.5;fill:none"></path><text x="559" y="195" style="font-size:11px;fill:#fbbf24;font-weight:600" text-anchor="middle">retention expires</text><text x="410" y="276" class="small" text-anchor="middle">retention window: 7–90 days, configurable per bucket</text><text x="700" y="262" class="small" text-anchor="middle">storage reclaimed</text></svg></div></figure>
<p>Retention is set per bucket. Turning soft delete off later doesn't purge data
that's already in the holding state. That data keeps aging out on the schedule
it was created with. While a bucket itself is in the holding state, its name
stays reserved. You can't reuse the name until the bucket is restored or fully
removed.</p>
<img src="https://www.tigrisdata.com/blog/assets/images/agentic-workflows-bf657e50abec8eaa3704b1af8cb0e121.webp" alt="Ty the tiger overseeing a vaulted stone scriptorium, where multiple colored enchanted quills work autonomously between reading tables inscribing scrolls. A translucent teal-and-gold layer of magical light hovers chest-high across the chamber, holding fallen scrolls safely suspended above the floor." style="width:100%;max-width:42rem;margin:1.5rem auto;display:block">
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-agent-writes-get-a-safety-net">Your agent writes get a safety net<a href="https://www.tigrisdata.com/blog/soft-delete/#your-agent-writes-get-a-safety-net" class="hash-link" aria-label="Direct link to Your agent writes get a safety net" title="Direct link to Your agent writes get a safety net" translate="no">​</a></h2>
<p>Tigris already gives you
<a href="https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/" target="_blank" rel="noopener noreferrer" class="">snapshots and forks</a>
when you want to isolate an agent's work into a copy of the bucket. Soft delete
adds another option for the cases where the agent or CI job writes directly to a
real bucket. Any delete it makes is recoverable inside the retention window. A
coding agent cleaning up stale prefixes, a pipeline rotating documents in a RAG
corpus, a CI job resetting test fixtures between runs. All of these can write
directly, and if something goes wrong, you click restore.</p>
<p>It also changes how you run destructive work against production. A lot of teams
keep a staging bucket around as an insurance policy for migrations, cleanups,
and retention sweeps that touch real data. You run the script there first, watch
what happens, point it at production only after you're sure. With soft delete
on, the bucket holds its own undo history. You can run the migration directly,
watch what breaks, and put back whatever you didn't mean to touch.</p>
<p>And it's not always about recovering from the last mistake. Every delete on a
key gets its own restorable version, stamped with the time it happened. So if a
bad write goes out and a string of good ones follow, you can still reach back
past the noise to the exact prior state you wanted. The good writes stay where
they are.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="every-version-is-its-own-restore-point">Every version is its own restore point<a href="https://www.tigrisdata.com/blog/soft-delete/#every-version-is-its-own-restore-point" class="hash-link" aria-label="Direct link to Every version is its own restore point" title="Direct link to Every version is its own restore point" translate="no">​</a></h2>
<p>Inside any bucket with soft delete on, the dashboard splits the file list into
<strong>All files</strong> and <strong>Deleted files</strong>. Pick a deleted object and the <strong>Version
history</strong> panel opens on the right. It lists every soft-deleted version of that
key with a timestamp, version ID, size, and ETag. Click restore on the one you
want and the object is live again.</p>
<p><em>Figure 1. The Version history panel for a soft-deleted object, listing each
version with its timestamp, version ID, size, and ETag. Each one is restorable
on its own.</em></p>
<img src="https://www.tigrisdata.com/blog/assets/images/version-history-b71d93a70143b69e23923f970be46ae7.webp" alt="The Version history panel in the Tigris dashboard, listing soft-deleted versions of an object with timestamps, version IDs, sizes, and ETags" style="cursor:zoom-in;width:100%;max-width:42rem;margin:1.5rem auto;display:block">
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="enabling-it-takes-one-toggle-one-header-or-one-command">Enabling it takes one toggle, one header, or one command<a href="https://www.tigrisdata.com/blog/soft-delete/#enabling-it-takes-one-toggle-one-header-or-one-command" class="hash-link" aria-label="Direct link to Enabling it takes one toggle, one header, or one command" title="Direct link to Enabling it takes one toggle, one header, or one command" translate="no">​</a></h2>
<p>In the dashboard, you can toggle <strong>Enable Soft Delete</strong> when you create a
bucket, or open <strong>Bucket Settings → Data Management</strong> on a bucket you already
have.</p>
<div style="display:flex;gap:1rem;max-width:42rem;margin:1.5rem auto;flex-wrap:wrap;align-items:flex-start"><div style="flex:1 1 0;min-width:240px"><p style="font-size:0.85rem;font-style:italic;text-align:center;margin-bottom:0.5rem"></p><p>Figure 2. Enabling soft delete in the Create Bucket dialog.</p><p></p><img src="https://www.tigrisdata.com/blog/assets/images/bucket-create-981b661567a90bfc471ad6d286b41d3e.webp" alt="The Create Bucket dialog in the Tigris dashboard with the Enable Soft Delete toggle switched on and a retention window of 7 days set" style="cursor:zoom-in;width:100%;height:320px;object-fit:contain;display:block;background:var(--ifm-background-surface-color, #fafafa);border-radius:0.25rem"></div><div style="flex:1 1 0;min-width:240px"><p style="font-size:0.85rem;font-style:italic;text-align:center;margin-bottom:0.5rem"></p><p>Figure 3. Enabling soft delete from Bucket Settings → Data Management.</p><p></p><img src="https://www.tigrisdata.com/blog/assets/images/bucket-update-4fe4fc6ce9800a381c11720ebaa59729.webp" alt="The Bucket Settings → Data Management tab in the Tigris dashboard with the Enable Soft Delete toggle switched on for an existing bucket" style="cursor:zoom-in;width:100%;height:320px;object-fit:contain;display:block;background:var(--ifm-background-surface-color, #fafafa);border-radius:0.25rem"></div></div>
<p>From the S3 API, set the <code>X-Tigris-Soft-Delete</code> header on <code>CreateBucket</code>. Use
<code>true</code> for the default retention of 7 days, or pass a number between 7 and 90
for a custom window.</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">createBucketWithSoftDelete</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> client </span><span class="token operator">*</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> bucketName </span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">error</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">CreateBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CreateBucketInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bucketName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options </span><span class="token operator">*</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">APIOptions </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">APIOptions</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> http</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">AddHeaderValue</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"X-Tigris-Soft-Delete"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"30"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> err</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>From the CLI, the same setting is a flag on <code>tigris buckets set</code>.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets set my-bucket --soft-delete enable --retention-days 30</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="restore-is-one-click-or-one-command">Restore is one click or one command<a href="https://www.tigrisdata.com/blog/soft-delete/#restore-is-one-click-or-one-command" class="hash-link" aria-label="Direct link to Restore is one click or one command" title="Direct link to Restore is one click or one command" translate="no">​</a></h2>
<p>On the buckets page, a <strong>Deleted buckets</strong> segment lists every soft-deleted
bucket that's still inside its retention window. Open the row's menu and click
restore. The bucket reappears in the regular list. Every object it held when you
deleted it is live and readable again.</p>
<p><em>Figure 4. The Deleted buckets segment in the Tigris dashboard, with a
soft-deleted bucket's menu opened to the Restore action.</em></p>
<img src="https://www.tigrisdata.com/blog/assets/images/deleted-buckets-tab-3ceecb32fdfd7a688982aee4ae13d2e6.webp" alt="The Deleted buckets tab in the Tigris dashboard, showing a soft-deleted bucket with the overflow menu open and a Restore option available" style="cursor:zoom-in;width:100%;max-width:42rem;margin:1.5rem auto;display:block">
<p>Objects work the same way. Open the <strong>Deleted files</strong> segment, pick a version,
click restore.</p>
<p>From the CLI, list soft-deleted buckets and restore one by name.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets list --deleted</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tigris buckets restore my-bucket</span><br></div></code></pre></div></div>
<p><code>tigris buckets delete my-bucket --yes</code> also respects the bucket's soft delete
setting. If soft delete is on, the bucket moves into the holding state and shows
up under <code>tigris buckets list --deleted</code> until you restore it or the retention
window expires.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-cost-wont-change">Your cost won't change<a href="https://www.tigrisdata.com/blog/soft-delete/#your-cost-wont-change" class="hash-link" aria-label="Direct link to Your cost won't change" title="Direct link to Your cost won't change" translate="no">​</a></h2>
<p>Data in the holding state is billed at the same per-GB rate as live data for as
long as it sits inside the retention window. There's no separate add-on fee for
turning the feature on. When the window expires and Tigris removes the data, the
storage is reclaimed and the bill goes down.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="soft-delete-is-recovery-not-backup">Soft delete is recovery, not backup<a href="https://www.tigrisdata.com/blog/soft-delete/#soft-delete-is-recovery-not-backup" class="hash-link" aria-label="Direct link to Soft delete is recovery, not backup" title="Direct link to Soft delete is recovery, not backup" translate="no">​</a></h2>
<p>Pick a retention window that matches the cycle your team actually operates on.
The default is 7 days, which is enough time for most teams to notice that
something went wrong and recover. If your incident review runs on a longer
cadence, or you want a wider buffer for audits, push the window out to 30 or 60
days. Already soft-deleted data keeps the retention it was created with, so
changing the window later only affects new deletes.</p>
<p>Soft delete is recovery, not backup. It protects you against the kind of mistake
you can catch inside the window you set. It doesn't protect you against a
long-running corruption that goes unnoticed for months, and it doesn't replace
whatever you do for long-term retention or disaster recovery. Use it alongside
those, not instead of them.</p>
<p>If you're on a snapshot-enabled bucket, soft delete sits underneath snapshots.
Deleting a specific version moves that version into the holding state and it
shows up in the <strong>Deleted files</strong> segment, where you can restore it the same way
as any other soft-deleted object. A plain delete still records a delete marker
and leaves earlier versions live, the same as before.</p>
<p>For the full dashboard walkthrough and the API reference, see the
<a href="https://www.tigrisdata.com/docs/buckets/soft-delete/" target="_blank" rel="noopener noreferrer" class="">soft delete documentation</a>.</p>
<img src="https://www.tigrisdata.com/blog/assets/images/soft-delete-c7716a5adf4e036d4f678c402d4210f6.webp" alt="Ty the tiger inside an ancient cliffside vault chamber, surveying rows of stone pedestals that hold glowing soft-deleted scroll-artifacts inscribed with timestamped runes, the deepest pedestals fading into dark mist as their retention expires, with the verdant Tigris kingdom visible through the cave mouth" style="width:100%;max-width:42rem;margin:1.5rem auto;display:block">
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Enable soft delete on your buckets today</span><p>Sign in to the Tigris console and toggle Soft Delete in Bucket Settings → Data Management. Your first 5 GB are free.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://console.storage.dev/signin" class="cta-link"><div>Open the Tigris console<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Updates</category>
            <category>Soft Delete</category>
            <category>Data Management</category>
            <category>Object Storage</category>
        </item>
        <item>
            <title><![CDATA[Giving your Go apps Tigris superpowers]]></title>
            <link>https://www.tigrisdata.com/blog/storage-sdk-go/</link>
            <guid>https://www.tigrisdata.com/blog/storage-sdk-go/</guid>
            <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The Go Storage SDK adds first-class methods for bucket forking, snapshots, and object renaming — plus a simpler high-level client for everyday storage code.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-fa7dad8a55523843ae7fb4d3932eb616.webp" class="hero-image" alt="A neon blue tiger walks down a city street at dusk, with skyscrapers and a crescent moon in the background">
<p>If you use Go for your web services and want to take advantage of Tigris'
advanced features like <a href="https://www.tigrisdata.com/docs/forks/" target="_blank" rel="noopener noreferrer" class="">bucket forking</a>,
<a href="https://www.tigrisdata.com/docs/snapshots/" target="_blank" rel="noopener noreferrer" class="">snapshots</a>, and
<a href="https://www.tigrisdata.com/docs/objects/object-rename/" target="_blank" rel="noopener noreferrer" class="">object renaming</a>,
normally your code has to look like this:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">WithRename</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token operator">*</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options </span><span class="token operator">*</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">APIOptions </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">APIOptions</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> http</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">AddHeaderValue</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"X-Tigris-Rename"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"true"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)">// rename the object in the bucket</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">CopyObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CopyObjectInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">     aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bucketName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  CopySource</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bucketName </span><span class="token operator">+</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"/"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> keyName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">        aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">targetName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">WithRename</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Fatalf</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Unable to rename object. Here's why: %v"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>With the new SDK, it looks like this:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token boolean">_</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">RenameObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">CopyObjectInput</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	Bucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">     aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bucketName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	CopySource</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bucketName </span><span class="token operator">+</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"/"</span><span class="token plain"> </span><span class="token operator">+</span><span class="token plain"> keyName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	Key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">        aws</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">String</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">targetName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	log</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Fatalf</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"Unable to rename object. Here's why: %v"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>The <a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0" target="_blank" rel="noopener noreferrer" class="">Go Storage SDK</a>
fixes this. It gives you dedicated methods for Tigris features, in two modes:</p>
<ul>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0" target="_blank" rel="noopener noreferrer" class="">package storage</a>:
Helper methods wrapping the AWS S3 client you already use.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0/simplestorage" target="_blank" rel="noopener noreferrer" class="">package simplestorage</a>:
A reimagined high-level object storage client for Go applications.</li>
</ul>
<!-- -->
<p>If you want to get started with it today, <code>go get</code> it:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">go get github.com/tigrisdata/storage-go@latest</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="an-sdk-so-nice-we-did-it-twice">An SDK so nice we did it twice<a href="https://www.tigrisdata.com/blog/storage-sdk-go/#an-sdk-so-nice-we-did-it-twice" class="hash-link" aria-label="Direct link to An SDK so nice we did it twice" title="Direct link to An SDK so nice we did it twice" translate="no">​</a></h2>
<p>One of the best ways to think about Tigris is that it's like S3, but more.
Tigris handles global replication for you. Tigris handles
<a href="https://www.tigrisdata.com/docs/migration/" target="_blank" rel="noopener noreferrer" class="">migrating your data for you</a>.
Tigris also lets you snapshot, fork, and
<a href="https://www.tigrisdata.com/docs/objects/bundle/" target="_blank" rel="noopener noreferrer" class="">download bundles of objects all at once</a>.
These operations extend S3, so the SDK extends the S3 client. Here's what you
get:</p>
<ul>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.BundleObjects" target="_blank" rel="noopener noreferrer" class="">BundleObjects</a>:
fetch <a href="https://www.tigrisdata.com/docs/objects/bundle/" target="_blank" rel="noopener noreferrer" class="">a bundle of objects</a>
all at once.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.CreateBucketFork" target="_blank" rel="noopener noreferrer" class="">CreateBucketFork</a>:
fork a bucket <a href="https://www.tigrisdata.com/docs/forks/" target="_blank" rel="noopener noreferrer" class="">into a new bucket</a> so
your agents have their own sandboxes.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.CreateBucketSnapshot" target="_blank" rel="noopener noreferrer" class="">CreateBucketSnapshot</a>:
create a new
<a href="https://www.tigrisdata.com/docs/snapshots/" target="_blank" rel="noopener noreferrer" class="">point-in-time snapshot</a> for your
buckets so you can save your data from digital destruction.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.CreateSnapshotEnabledBucket" target="_blank" rel="noopener noreferrer" class="">CreateSnapshotEnabledBucket</a>:
create a new bucket with <a href="https://www.tigrisdata.com/docs/forks/" target="_blank" rel="noopener noreferrer" class="">forking</a> and
<a href="https://www.tigrisdata.com/docs/snapshots/" target="_blank" rel="noopener noreferrer" class="">snapshots</a> enabled.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.HeadBucketForkOrSnapshot" target="_blank" rel="noopener noreferrer" class="">HeadBucketForkOrSnapshot</a>:
fetches the fork/snapshot metadata for a bucket.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.ListBucketSnapshots" target="_blank" rel="noopener noreferrer" class="">ListBucketSnapshots</a>:
list all the snapshots for a bucket so you can create forks from those
snapshots.</li>
<li class=""><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0#Client.RenameObject" target="_blank" rel="noopener noreferrer" class="">RenameObject</a>:
<a href="https://www.tigrisdata.com/docs/objects/object-rename/" target="_blank" rel="noopener noreferrer" class="">rename (move)</a> an
object from one place in your bucket to another.</li>
</ul>
<p>As Tigris gets more features, we plan to just add more methods.</p>
<p>This mode is designed to be a <em>drop-in replacement</em> for your existing S3 client
to make migration trivial. When I moved some of my own projects over from the
AWS S3 library to package storage, it took about 30 seconds at most. Everything
compiled normally, everything worked as expected, and I got access to the extra
features I needed. Win/win/win!</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="but-wait-theres-more">But wait, there's more<a href="https://www.tigrisdata.com/blog/storage-sdk-go/#but-wait-theres-more" class="hash-link" aria-label="Direct link to But wait, there's more" title="Direct link to But wait, there's more" translate="no">​</a></h3>
<p>Honestly, we could have stopped there, but we didn't. Alongside this I also
added
<a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0/simplestorage" target="_blank" rel="noopener noreferrer" class="">package simplestorage</a>:
a brand new interface to object storage. Most of the time your apps end up using
a single bucket. Why should you have to pass the bucket name every time you do
anything? Take a gander at how easy it is to copy a file from Tigris to your
local filesystem:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> simplestorage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">panic</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">obj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my/key"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">panic</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">defer</span><span class="token plain"> obj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Close</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">slog</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Info</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"object metadata"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"key"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my/key"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"size"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> obj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Size</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token string" style="color:rgb(255, 121, 198)">"content-type"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> obj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">ContentType</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">fout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> os</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Create</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"./var/object-data"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">panic</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">defer</span><span class="token plain"> fout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Close</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">io</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Copy</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">fout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> obj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The bucket name and credentials were inferred from environment variables:</p>
<ul>
<li class=""><code>TIGRIS_STORAGE_BUCKET</code>: The default bucket to operate on.</li>
<li class=""><code>TIGRIS_STORAGE_ACCESS_KEY_ID</code>: The access key ID for your app's
<a href="https://www.tigrisdata.com/docs/iam/manage-access-key/" target="_blank" rel="noopener noreferrer" class="">keypair</a>.</li>
<li class=""><code>TIGRIS_STORAGE_SECRET_ACCESS_KEY</code>: The secret access key for your app's
<a href="https://www.tigrisdata.com/docs/iam/manage-access-key/" target="_blank" rel="noopener noreferrer" class="">keypair</a>.</li>
</ul>
<p>If you don't want to change the environment variable names, simplestorage will
use the standard
<a href="https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html" target="_blank" rel="noopener noreferrer" class="">AWS configuration resolution flow</a>
you're already used to.</p>
<p>Forked buckets work with the
<a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.6.0/simplestorage#Client.For" target="_blank" rel="noopener noreferrer" class=""><code>For</code></a>
method:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">newBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ForkBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"my-agents-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">panic</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">newBucketClient </span><span class="token operator">:=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">For</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">newBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)">// do whatever you want here</span><br></div></code></pre></div></div>
<p>The main innovation here is reducing cognitive load so that you can focus on
what you're doing with your buckets instead of plumbing the minutiae of your
object storage API being automatically generated from Java classes. There's also
nothing stopping you from using this with AWS S3 or another object storage
provider like
<a href="https://www.hetzner.com/storage/object-storage/" target="_blank" rel="noopener noreferrer" class="">Hetzner object storage</a>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">s3Client</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> simplestorage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  simplestorage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithRegion</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"fsn1"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  simplestorage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithEndpoint</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"https://fsn1.your-objectstorage.com"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  simplestorage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithAccessKeypair</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">accessKeyID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> secretAccessKey</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">panic</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Advanced Tigris features like bucket forking will not work (Hetzner doesn't
implement our extensions to the S3 API), but basic object manipulation will work
just fine.</p>
<p>Note that any <em>object</em> manipulation functions will use the client's default
bucket, but any <em>bucket</em> manipulation functions require you to spell out the
bucket's name just to be sure you're operating on the right bucket.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="get-go-ing-today">Get <code>go</code>-ing today<a href="https://www.tigrisdata.com/blog/storage-sdk-go/#get-go-ing-today" class="hash-link" aria-label="Direct link to get-go-ing-today" title="Direct link to get-go-ing-today" translate="no">​</a></h2>
<p>If you want to try this out, install it in your Go project with <code>go get</code>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">go get github.com/tigrisdata/storage-go@latest</span><br></div></code></pre></div></div>
<p>Please give us feedback
<a href="https://github.com/tigrisdata/storage-go" target="_blank" rel="noopener noreferrer" class="">in the storage-go repo</a>. We want to
make this the best way to use Tigris for Go developers and your feedback can
only make it better.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Ready to supercharge your Go apps?</span><p>The Go Storage SDK gives you first-class access to bucket forking, snapshots, and more — with less boilerplate than raw S3 calls.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://pkg.go.dev/github.com/tigrisdata/storage-go@v0.7.0" class="cta-link"><div>Read the SDK docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Updates</category>
            <category>Engineering</category>
            <category>Build with Tigris</category>
        </item>
        <item>
            <title><![CDATA[Introducing storagesdk.dev]]></title>
            <link>https://www.tigrisdata.com/blog/storagesdk/</link>
            <guid>https://www.tigrisdata.com/blog/storagesdk/</guid>
            <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[StorageSDK is a provider-agnostic storage API for Node.js: one interface
across S3, R2, Azure, GCS, Tigris, and more, with snapshots and forks built in.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-b1e17c7bc411e66ab1bc77ec8db3f529.webp" class="hero-image" alt="Introducing storagesdk.dev — a multi-provider TypeScript SDK with fork and snapshot as primitives, shown connecting to Azure, Google Cloud Storage, Cloudflare R2, Vercel Blob Storage, Tigris, MinIO, and the local filesystem.">
<p>You've spent fifteen years branching, tagging, and rebasing your code without
thinking twice. Then you reach for object storage and the API is PUT, GET, LIST,
DELETE, and "good luck." Buckets are the one piece of your stack you can't
safely fork, can't tag a known-good state on, can't mutate on a side branch
without a sinking feeling.</p>
<p><a href="https://storagesdk.dev/" target="_blank" rel="noopener noreferrer" class="">StorageSDK</a> is what happens when you decide that's a
bug. It's a TypeScript SDK for object storage with snapshots and forks as
first-class primitives: branch a bucket per agent run, mutate safely, replay
from the same baseline. Same API on top of whichever backend you pick.</p>
<!-- -->
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-other-half-of-the-picture">The other half of the picture<a href="https://www.tigrisdata.com/blog/storagesdk/#the-other-half-of-the-picture" class="hash-link" aria-label="Direct link to The other half of the picture" title="Direct link to The other half of the picture" translate="no">​</a></h2>
<p><a href="https://computesdk.dev/" target="_blank" rel="noopener noreferrer" class="">ComputeSDK</a> is a vendor-neutral project that gives you
one consistent API to control sandboxes across every major sandbox provider.
We've partnered with the ComputeSDK team to launch StorageSDK as the matching
half — same shape, same goal, but for storage instead of compute. Pick a
backend, get out of your way.</p>
<p>The thesis underneath both: <strong>Git === storage</strong>. Not literally. Conceptually.
Buckets need what Git gave us fifteen years ago — tags, branches, isolated
mutations, replayable state. Once you've spent a career with those primitives in
your VCS, going back to a flat object store feels like editing without undo.</p>
<p>The GitHub adapter is where the metaphor stops being a metaphor:
<code>storage.snapshots.create()</code> makes a tag, <code>storage.forks.create()</code> makes a
branch, and <code>storage.forks.get(name)</code> hands you a writable storage handle on
that branch. The rest of this post is about applying the same idea to every
other backend.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-interface-many-backends">One interface. Many backends.<a href="https://www.tigrisdata.com/blog/storagesdk/#one-interface-many-backends" class="hash-link" aria-label="Direct link to One interface. Many backends." title="Direct link to One interface. Many backends." translate="no">​</a></h2>
<p>StorageSDK gives you one interface across basic operations that are portable
across every storage provider: get, put, list, delete, copy, and make a URL.
Changing backends is one import and one adapter config — swap the tab to see for
yourself:</p>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">Tigris</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Amazon S3</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Cloudflare R2</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Google Cloud Storage</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Vercel Blob</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">GitHub</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Local filesystem</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> tigris </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/tigris"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">tigris</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bucket</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    accessKeyId</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">TIGRIS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    secretAccessKey</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">TIGRIS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">upload</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"hello.txt"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Hello, storage SDK!"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  contentType</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"text/plain"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> text </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">download</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"hello.txt"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"text"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> s3 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/s3"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">s3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bucket</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    region</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"us-east-1"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    credentials</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      accessKeyId</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      secretAccessKey</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> r2 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/r2"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">r2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bucket</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    accountId</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">R2_ACCOUNT_ID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    accessKeyId</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">R2_ACCESS_KEY_ID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    secretAccessKey</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">R2_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> gcs </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/gcs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">gcs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bucket</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    projectId</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">GOOGLE_PROJECT_ID</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    keyFilename</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">GOOGLE_APPLICATION_CREDENTIALS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> vercel </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/vercel"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">vercel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bucket</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    token</span><span class="token operator">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">env</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">BLOB_READ_WRITE_TOKEN</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> github </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/github"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">github</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    owner</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"storagesdk"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    repo</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-artifacts"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// branch defaults to the repo's default branch</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// token defaults to process.env.GITHUB_TOKEN</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> Storage </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/core"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> fs </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"@storagesdk/adapters/fs"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> storage </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name">Storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  adapter</span><span class="token operator">:</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">fs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> root</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"./.storage"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> folder</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-runs"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">upload</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"hello.txt"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Hello, storage SDK!"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> text </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">download</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"hello.txt"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"text"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div></div></div></div>
<p>The rest of your code doesn't need to see the man behind the curtain. It just
stores and reads files. You don't need to worry about the cognitive overload
involved to set up storage with other libraries. Put that effort into your real
life's mission: changing the world in the form of business to business software
as a service applications.</p>
<p>This kind of simplicity also helps your agents adapt to the changing needs of
your storage architecture because they don't have to care about how storage
works as long as it does.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="portable-but-actually-this-time">Portable, but actually this time<a href="https://www.tigrisdata.com/blog/storagesdk/#portable-but-actually-this-time" class="hash-link" aria-label="Direct link to Portable, but actually this time" title="Direct link to Portable, but actually this time" translate="no">​</a></h2>
<p>Here's all the providers we support out of the gate:</p>
<ul>
<li class=""><a href="https://storagesdk.dev/adapters/s3/" target="_blank" rel="noopener noreferrer" class=""><strong>Amazon S3</strong></a> - Normal S3 just in case
you actually need it.</li>
<li class=""><a href="https://storagesdk.dev/adapters/azure/" target="_blank" rel="noopener noreferrer" class=""><strong>Azure Blob Storage</strong></a> - We don't
want to deal with Azure's API either.</li>
<li class=""><a href="https://storagesdk.dev/adapters/r2/" target="_blank" rel="noopener noreferrer" class=""><strong>Cloudflare R2</strong></a> - Store your files in
Cloudflare's global storage network.</li>
<li class=""><a href="https://storagesdk.dev/adapters/fly/" target="_blank" rel="noopener noreferrer" class=""><strong>Fly.io</strong></a> - Your app is already
global, why can't your storage also be global?</li>
<li class=""><a href="https://storagesdk.dev/adapters/github/" target="_blank" rel="noopener noreferrer" class=""><strong>GitHub</strong></a> - Git stores objects
internally, it only makes sense to have an object storage backend powered by
GitHub!</li>
<li class=""><a href="https://storagesdk.dev/adapters/gcs/" target="_blank" rel="noopener noreferrer" class=""><strong>Google Cloud Storage</strong></a> - Good in
case you need to store 1e100 objects in the cloud.</li>
<li class=""><a href="https://storagesdk.dev/adapters/fs/" target="_blank" rel="noopener noreferrer" class=""><strong>Local Filesystem</strong></a> - Store your files
on your local filesystem for development or if you only have one server and
don't plan to expand.</li>
<li class=""><a href="https://storagesdk.dev/adapters/minio/" target="_blank" rel="noopener noreferrer" class=""><strong>MinIO AIStor</strong></a> - Object storage
you can run on machines you can look at.</li>
<li class=""><a href="https://storagesdk.dev/adapters/railway/" target="_blank" rel="noopener noreferrer" class=""><strong>Railway Buckets</strong></a> - All aboard
with object storage!</li>
<li class=""><a href="https://storagesdk.dev/adapters/tigris/" target="_blank" rel="noopener noreferrer" class=""><strong>Tigris</strong></a> - The innovator of
bucket snapshots, forking, and more.</li>
<li class=""><a href="https://storagesdk.dev/adapters/vercel/" target="_blank" rel="noopener noreferrer" class=""><strong>Vercel Blob Storage</strong></a> - Useful
for getting your next.js app into prod with the same API as development</li>
<li class=""><a href="https://storagesdk.dev/adapters/write-your-own/" target="_blank" rel="noopener noreferrer" class=""><strong>Write your own</strong></a> -
Missing your favourite set of buzzwords? Here's the square peg you need to
adapt to your round hole of choice.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bed-bath-and-beyond">Bed, Bath, and Beyond<a href="https://www.tigrisdata.com/blog/storagesdk/#bed-bath-and-beyond" class="hash-link" aria-label="Direct link to Bed, Bath, and Beyond" title="Direct link to Bed, Bath, and Beyond" translate="no">​</a></h2>
<p>We could have stopped here and it would have been pretty great. This general
shape of problem and solution really does meet the needs of developers building
with object storage. You've got the bed, you've got the bath, but what about the
beyond? What if your storage SDK also gave you the ability to time travel?
StorageSDK lets you do that, no creepy remote control required.</p>
<p>We made snapshots and bucket forking into a first-class provider-agnostic
feature:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> snap </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">snapshots</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">create</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"baseline"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">forks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">create</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"agent-run-123"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  fromSnapshot</span><span class="token operator">:</span><span class="token plain"> snap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">id</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> fork </span><span class="token operator">=</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">forks</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"agent-run-123"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> fork</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">upload</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"output.json"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> result</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<p>Branch a bucket per agent run. Mutate safely. Replay from the same baseline. The
mapping holds across every adapter:</p>
<ul>
<li class="">snapshot = tag</li>
<li class="">fork = branch</li>
<li class="">parent bucket = mainline state</li>
<li class="">forked bucket = writable isolated workspace</li>
</ul>
<p>It works on GitHub, Tigris, S3, GCS, and more on the way.</p>
<p>Tigris lets you have this out of the box, but what about Google Cloud Storage or
S3? How do you get that there?</p>
<p>We made this work with sibling buckets using CopyObject to transparently make
things Just Work™. When you make a snapshot, StorageSDK makes a copy of all of
the data so that you get your snapshots as normal objects. This is similar to
what Tigris does under the hood with object metadata instead of the actual
object data.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-escape-hatch">The escape hatch<a href="https://www.tigrisdata.com/blog/storagesdk/#the-escape-hatch" class="hash-link" aria-label="Direct link to The escape hatch" title="Direct link to The escape hatch" translate="no">​</a></h3>
<p>Need to do something advanced that the StorageSDK doesn't provide? Break out
into the raw provider object: <code>storage.raw</code>. On the Tigris adapter, every
value-namespace export of <code>@tigrisdata/storage</code> — functions, constants, the
<code>UploadAction</code> enum — is accessible on <code>storage.raw</code> with the adapter's auth,
endpoint, and bucket already injected. Call them as if you imported them from
<code>@tigrisdata/storage</code> directly:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> storage</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">raw</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">setBucketLifecycle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"my-bucket"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  lifecycleRules</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> expiration</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> days</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">30</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<p>Per-call <code>config</code> overrides merge on top of the adapter's resolved config (user
wins, adapter fills the gaps). Swap the adapter and <code>storage.raw</code> becomes the
matching native client for that backend — fully typed, no casts, no <code>any</code>, no
losing your provider when you go off the paved road.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-future-looks-bright">The future looks bright<a href="https://www.tigrisdata.com/blog/storagesdk/#the-future-looks-bright" class="hash-link" aria-label="Direct link to The future looks bright" title="Direct link to The future looks bright" translate="no">​</a></h2>
<p>Most real workloads eventually need snapshots and forks: pre-migration
snapshots, isolated experiment branches, point-in-time reads, a bucket per agent
run that you can throw away when the run ends. Building those on top of a flat
object API every time is the kind of work that should already be solved. Storage
SDK solves it once, with the same API on every backend it supports — and that
solution doesn't get uglier when you swap the provider underneath.</p>
<p>Branch a bucket. Mutate freely. Throw the branch away when you're done, or
replay it onto a new baseline. The shape should feel obvious to anyone who's
spent fifteen years on the Git side of the analogy. That's the whole point.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Ready to treat storage like Git?</span><p>StorageSDK gives you the same primitives developers already rely on: tags, branches, isolated mutations, and replayable state.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://storagesdk.dev/" class="cta-link"><div>Get started with StorageSDK<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Updates</category>
            <category>Engineering</category>
            <category>Object Storage</category>
            <category>Node.js</category>
            <category>S3</category>
        </item>
        <item>
            <title><![CDATA[Give your agents disposable environments in Go]]></title>
            <link>https://www.tigrisdata.com/blog/agent-sandbox-go/</link>
            <guid>https://www.tigrisdata.com/blog/agent-sandbox-go/</guid>
            <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Kefka is a userspace shell sandbox in Go that gives every AI agent its own copy-on-write Tigris bucket fork plus Python, jq, and ripgrep via WebAssembly.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-b01ff9594ec54b01e35a5029a4b5cfa0.webp" class="hero-image" alt="A mad god surrounded by robotic minions, evoking Kefka from Final Fantasy VI commanding a horde of AI agents.">
<div style="background-color:var(--docs-color-background-100);border:1px solid var(--docs-color-border);border-radius:12px;padding:1.5rem;max-width:42rem;margin:1.5rem auto;position:relative;overflow:hidden"><div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem"><div style="background-color:var(--ifm-color-primary);color:white;border-radius:10px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;flex-shrink:0"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 10 10-12h-9l1-10z" fill="currentColor"></path></svg></div><span style="font-weight:700;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--docs-color-text)">Quick Summary</span><span style="margin-left:auto;font-size:13px;color:var(--docs-color-text-100);font-weight:500">6 min read</span></div><div style="display:flex;flex-direction:column;gap:1rem"><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Userspace sandbox in Go.</strong> <span style="color:var(--docs-color-text-100)">A real shell interpreter (mvdan.cc/sh) plus ported coreutils entirely in userspace, so you can multiplex hundreds of agent sessions on one server without containers, VMs, or extra kernel overhead.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">A bucket fork per agent.</strong> <span style="color:var(--docs-color-text-100)">Every session gets its own copy-on-write Tigris bucket fork as its filesystem. Whatever the agent does stays in the fork, and the fork is force-deleted the moment it disconnects.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Python, jq, and ripgrep via WebAssembly.</strong> <span style="color:var(--docs-color-text-100)">Compiling these tools to WebAssembly lets Kefka inject the agent's workspace as the filesystem, so agents run whatever scripts they want without touching the host.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">POSIX-checked compatibility.</strong> <span style="color:var(--docs-color-text-100)">The ported commands were scanned against the POSIX 2018 spec to generate a conformance report that shows exactly where the gaps are.</span></div></div></div><div style="position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg, var(--ifm-color-primary), #7c5cfc)"></div></div>
<p>Agents need disposable environments because the blast radius of things going
wrong is way too big. Nobody wants "what if our expense-submission agent yeets
all the receipts out of the bucket?" showing up on a threat model. But how do
you safely give these agents access to shells?</p>
<!-- -->
<p>You'd need to sandbox the agents so that they can only affect their own little
storage world. This is doable with conventional tooling, but that usually
requires giving your executors way more permissions than they need. It gets
worse with modern stacks where a single agent loop spans multiple machines. How
do you keep that agent's storage consistent across all of them?</p>
<p>How do you secure a server with a <code>bash</code> tool against an agent that has no
innate sense for if running <code>rm -rf /</code> is really a good idea or not?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="well-the-thing-is-you-dont">Well, the thing is you don’t<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#well-the-thing-is-you-dont" class="hash-link" aria-label="Direct link to Well, the thing is you don’t" title="Direct link to Well, the thing is you don’t" translate="no">​</a></h2>
<p>Sure, sure, you can just throw in containers, vms, microvms, kata containers, or
pick your favourite buzzword and it’ll probably be fine if you have a known
number of agents. The way things are going means that we’re going to have
dynamic amounts of agents that can and will be more than your container engine,
VM software, or even kernel overhead will let you handle properly. To keep up,
we need infrastructure that can safely multiplex tens or hundreds of
simultaneous agent sessions onto the same server. All of them also need to be
sandboxed from the other so that they don't step on eachother's toes.</p>
<p>For context, this kind of sandboxing already exists if you're in the
JavaScript/TypeScript ecosystem with
<a href="https://www.tigrisdata.com/docs/ai/agent-shell/" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>
powered by <a href="https://justbash.dev/" target="_blank" rel="noopener noreferrer" class="">just-bash</a>. If you are looking for something
production-capable, you should probably start there.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dancing-mad-with-sandboxes">Dancing mad with sandboxes<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#dancing-mad-with-sandboxes" class="hash-link" aria-label="Direct link to Dancing mad with sandboxes" title="Direct link to Dancing mad with sandboxes" translate="no">​</a></h2>
<p>However, I'm not mainly in the JavaScript/TypeScript ecosystem, I'm a Go
developer and Go developers make agents too. In order to play with these ideas
in a language I'm more comfortable in, I made a library named
<a href="https://tangled.org/xeiaso.net/kefka" target="_blank" rel="noopener noreferrer" class="">Kefka</a> that can help you create this new
generation of infrastructure by making a sandbox purely in userspace using Go
and Tigris <a href="https://www.tigrisdata.com/docs/forks/" target="_blank" rel="noopener noreferrer" class="">bucket forks</a> so that every
agent has its own bucket with its own data. Any commands the agent runs stay
safely in the sandbox. If it needs, the agent can even shell out to python to do
any kind of complicated data analysis it needs. This is built off of the
foundation of <a href="https://www.tigrisdata.com/blog/agent-kit/" target="_blank" rel="noopener noreferrer" class="">agent workspaces</a>
from agent-kit, but using forks instead of empty buckets because I found forks a
bit more useful for my testing.</p>
<p>When I wrote Kefka, I started with a few components and just put them into a
blender:</p>
<ul>
<li class=""><a href="https://pkg.go.dev/mvdan.cc/sh/v3" target="_blank" rel="noopener noreferrer" class=""><strong>mvdan.cc/sh</strong></a>: a compatible shell
interpreter written in Go. This makes this agent-native shell use <em>a real
shell interpreter</em> instead of making up logic that may not be compatible with
what agents are trained on.</li>
<li class=""><a href="https://pkg.go.dev/github.com/go-git/go-billy/v5" target="_blank" rel="noopener noreferrer" class=""><strong>billy</strong></a>: a more complete
filesystem abstraction layer for Go applications. Go ships
<a href="https://pkg.go.dev/io/fs#FS" target="_blank" rel="noopener noreferrer" class="">a filesystem interface in the standard library</a>,
but it’s not complete enough to handle the nuance required for agents. Or file
writes. That's a separate topic.</li>
<li class=""><a href="https://github.com/vercel-labs/just-bash/tree/main/packages/just-bash" target="_blank" rel="noopener noreferrer" class=""><strong>the source code of just-bash</strong></a>:
everything in just-bash works enough already and I have a Claude Max 20x
subscription, surely I can just have Claude do most of the grunt work porting
commands over, right?</li>
</ul>
<p>Once I outlined the basic flow and interfaces, I tried having Claude port over
the implementation of
<a href="https://github.com/vercel-labs/just-bash/blob/main/packages/just-bash/src/commands/ls/ls.ts" target="_blank" rel="noopener noreferrer" class="">ls</a>
to
<a href="https://tangled.org/xeiaso.net/kefka/blob/main/command/internal/ls/ls.go" target="_blank" rel="noopener noreferrer" class="">its home in the Kefka repo</a>.
To my shock, surprise, and horror, it worked perfectly on the first try. This
makes sense, transformer models were
<a href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener noreferrer" class="">designed by the Google Translate team</a> for
translation-shaped tasks. What is porting stuff from JavaScript to Go other than
translation? After that I took that context window and converted it into
<a href="https://tangled.org/xeiaso.net/kefka/blob/main/.claude/skills/just-bash-port/SKILL.md" target="_blank" rel="noopener noreferrer" class="">a claude skill</a>
that I ran in parallel. With 5 agents going at once, I managed to cover the
critical set of coreutils (<code>cat</code>, <code>ls</code>, etc) and some of the extended commands
that you end up using in practice (<code>sha256sum</code>, <code>nl</code>, <code>du</code>, etc). After hooking
up a simple interactive shell REPL as a test, I tried using it as normal and it
was compliant with my muscle memory.</p>
<p>The porting process was mostly autonomous with each of the agents fighting for
supremacy in a single git checkout. It took most of a workday to get everything
ported over. Since most of it was async, I didn't have to babysit and could do
other things in the meantime. During this I ended up blowing through two Claude
Max 20x rate limit windows and ended up with
<a href="https://tangled.org/xeiaso.net/kefka/tree/main/command/internal" target="_blank" rel="noopener noreferrer" class="">one of the biggest list of Go subpackages I’ve ever seen</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="running-python">Running Python<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#running-python" class="hash-link" aria-label="Direct link to Running Python" title="Direct link to Running Python" translate="no">​</a></h2>
<p>At this point I had a filesystem, I had mostly-compatible commands, all I was
really missing was a couple real-world applications to run in this environment.
Language models are really good at writing Python scripts, so I figured it
wouldn’t be that hard to get Python ported over. I didn’t want to rewrite Python
in Go or do some kind of source-based translation from C to Go, so I picked
something more fun: <a href="https://webassembly.org/" target="_blank" rel="noopener noreferrer" class="">WebAssembly</a>.</p>
<p>WebAssembly is a vendor-neutral bytecode format, and that makes it great for
agent sandboxes. When you compile an application to WebAssembly, you have to
<em>explicitly inject</em> dependencies such as the filesystem, network stack, and
implementation of time. This means that if I had Python compiled to WebAssembly
I could hook up the agent workspace as a filesystem and rig input/output to the
AI agent workflow. This would sandbox the agent away so that it could run
<em>whatever Python scripts it wanted</em> without touching the host filesystem.</p>
<p>This ended up being surprisingly little code:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)">// from https://tangled.org/xeiaso.net/kefka/blob/main/command/internal/python3/python3.go</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Impl</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">Exec</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> ec </span><span class="token operator">*</span><span class="token plain">command</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">ExecContext</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> args </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">error</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	fsConfig </span><span class="token operator">:=</span><span class="token plain"> wazero</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">NewFSConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">sysfs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">FSConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithSysFSMount</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">billyfs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">New</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ec</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">FS</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"/"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	config </span><span class="token operator">:=</span><span class="token plain"> wazero</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">NewModuleConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token comment" style="color:rgb(98, 114, 164)">// Pipe ExecContext stdio</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithStdin</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ec</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Stdin</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithStdout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ec</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Stdout</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithStderr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ec</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">Stderr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token comment" style="color:rgb(98, 114, 164)">// Pipe argv</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithArgs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token function" style="color:rgb(80, 250, 123)">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"python3"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> args</span><span class="token operator">...</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token operator">...</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithName</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"python3"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token comment" style="color:rgb(98, 114, 164)">// Pipe filesystem</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithFSConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">fsConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token comment" style="color:rgb(98, 114, 164)">// Pipe system time</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token function" style="color:rgb(80, 250, 123)">WithSysNanosleep</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithSysNanotime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">WithSysWalltime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	mod</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> err </span><span class="token operator">:=</span><span class="token plain"> runtime</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">InstantiateModule</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> compiled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> err </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// Fit the square peg into the round hole</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> exitErr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> ok </span><span class="token operator">:=</span><span class="token plain"> errors</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">AsType</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token operator">*</span><span class="token plain">wsys</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">ExitError</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> ok </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">			</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> code </span><span class="token operator">:=</span><span class="token plain"> exitErr</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ExitCode</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> code </span><span class="token operator">!=</span><span class="token plain"> </span><span class="token number">0</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">				</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> interp</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">ExitStatus</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token function" style="color:rgb(80, 250, 123)">uint8</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">code</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">			</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">			</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> </span><span class="token boolean">nil</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">		</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> err</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">	</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> mod</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">Close</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Most of the code is just adapting the WebAssembly runtime types to the types the
rest of the environment uses. It's dependency injection with extra steps.</p>
<p>This basic flow is how I got <code>jq</code>, <code>ripgrep</code>, and <code>quickjs</code> ported over to this
environment so that agents (and humans!) have their choice of familiar tools to
dive into forks and shred the contents apart for buckety goodness.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="making-sure-its-compatible">Making sure it’s compatible<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#making-sure-its-compatible" class="hash-link" aria-label="Direct link to Making sure it’s compatible" title="Direct link to Making sure it’s compatible" translate="no">​</a></h2>
<p>Muscle memory is a kind of test, but it's not a substitute for actually
conforming to the
<a href="https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/" target="_blank" rel="noopener noreferrer" class="">POSIX specification</a>.
POSIX (the Portable Operating System Interface) is the formal set of
specifications that every shell and set of core utilities is implemented
against. Agents are also good at turning a spec into code. I wanted to see how
far I could get by feeding the POSIX spec to Claude Code.</p>
<p>To do that I downloaded the POSIX 2018 specifications as a zipfile and had
<a href="https://pandoc.org/" target="_blank" rel="noopener noreferrer" class="">pandoc</a> convert the relevant files from HTML to markdown.
If you’ve never used pandoc before, it’s a swiss army knife of plaintext file
conversion. You have HTML but you want markdown?
<code>pandoc --from html --to markdown</code>. Bam, job’s done. At a past job it’s how I
submitted the markdown blog drafts I wrote in Emacs to the marketing team’s use
of Microsoft Word and Wordpress.</p>
<p>With the spec in hand, I dumped all the relevant files into a
<a href="https://tangled.org/xeiaso.net/kefka/tree/main/docs/posix2018" target="_blank" rel="noopener noreferrer" class="">posix2018 folder</a>
and told Claude to go to town scanning over my implementation vs the specs. It
also generated
<a href="https://tangled.org/xeiaso.net/kefka/blob/main/docs/posix2018/CONFORMANCE.md" target="_blank" rel="noopener noreferrer" class="">a conformance report</a>
that could be used to guide future development. What I have right now covers the
muscle-memory commands I actually use, and the report tells me exactly where the
gaps are when an agent asks for something I haven't implemented yet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-demo">A demo<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#a-demo" class="hash-link" aria-label="Direct link to A demo" title="Direct link to A demo" translate="no">​</a></h2>
<p>One of the neat parts about implementing this in Go is the Go ecosystem’s mass
of libraries that handle low-level things such as SSH servers. Using
<a href="https://pkg.go.dev/github.com/gliderlabs/ssh" target="_blank" rel="noopener noreferrer" class="">gliderlabs’ SSH server package</a>,
I managed to create an SSH server sandboxed in Kefka so that any users
automatically get put into their own bucket fork. <code>ssh</code> in and do <em>whatever you
want</em>, you can’t hurt any of the data. Here’s a demo:</p>
<video controls="" autoplay="" loop="" muted="" playsinline="" style="width:100%;max-width:42rem;display:block;margin:0 auto"><source src="/blog/img/blog/agent-sandbox-go/sophia-demo.mp4" type="video/mp4"><p>Download the <a href="https://www.tigrisdata.com/blog/img/blog/agent-sandbox-go/sophia-demo.mp4">
MP4
</a> version.</p></video>
<p>And if you want to try it for yourself, dive in with your terminal:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">ssh sophia.xeiaso.net</span><br></div></code></pre></div></div>
<p>For extra fun, try running <code>snapshot --help</code>!</p>
<p>Under the hood it uses a flow like this:</p>
<!-- -->
<button type="button" aria-label="Sophia SSH session flow diagram. Click to enlarge." aria-haspopup="dialog" class="trigger_xMRY"><svg xmlns="http://www.w3.org/2000/svg" width="1820" height="855" viewBox="0 0 1820 855" role="img" aria-label="Sophia SSH session flow: connect, mint a UUIDv7 session ID, fork the configured bucket, mount the fork as the session filesystem, then force-delete the fork on disconnect." style="width:100%;height:auto;display:block"><defs><style>@font-face{font-family:"Virgil";src:url(data:font/woff2;base64,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)format("woff2");font-weight:400;font-style:normal}</style></defs><rect width="100%" height="100%" fill="#0e1920"></rect><text x="540" y="47" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="22" transform="translate(0 15)"><tspan x="540" dy="0">sophia: SSH shell with per-session Tigris bucket</tspan></text><path fill="none" stroke="#1e3340" stroke-dasharray="12 8" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M39.78 94.94c57.27 1.59 112.9.22 280.54-.87m-280.55.57c68.09 1.13 135.51.26 280.11.75m.55-.99c-.13 169.99.42 338.49-.12 720.7m-.47-720.34c-1.11 242.44-1.09 484.84.09 720.12m.15 1.33c-64.81 2.13-130.68 1.29-278.8-.3m278.04-.36c-97.2-2.57-195.89-1.45-278.65-1.25m-.45 1.27c-1.31-270-1.1-539.24-.55-720.73m.1 720.56c-.65-236.35-.25-472.37.18-720.01"></path><text x="60" y="109" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="60" dy="0">client</tspan></text><path fill="#142229" d="m69.6 334.69 218.58 1.55.83 107.7-217.31 1.19"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M71.24 336.61c43.39-2.42 86.56-2.05 217.74-1.98m-218.63.42c63.1-1.22 124.66-1.46 220.24-.04m-.44-1.01c-2.69 29.62.08 59.84-.59 109.52m.63-108.72c.06 29.85.16 59.5.44 110.61m-2.18.58c-57.87 1.9-114.67.01-217.28-.66m218.16.04c-65.57-1.36-132.78-.56-218.67-1.09m-1.54 1.59c.03-39.18 1.1-78.54 1.25-109.69m-.5 109.81c.62-28.66.64-57.71.89-110.04"></path><text x="100" y="363" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="100" dy="0">SSH client</tspan></text><text x="95" y="393" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="13" transform="translate(0 15)"><tspan x="95" dy="0">ssh -p 2222</tspan></text><text x="95" y="415" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="13" transform="translate(0 15)"><tspan x="95" dy="0">anyone@host</tspan></text><path fill="none" stroke="#1e3340" stroke-dasharray="12 8" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M519.43 94.86c144.12 3.6 288.79 3.22 719.8.91m-719.02-.91c153.28 1.76 307.2 1.88 719.47.35m.87-.01c.95 269.14.51 540.47-.59 720.32m.03-720.92c.2 205.01.3 409.01-.36 720.61m.58-.26c-256.93 2.65-513.43 2.07-720.06.84m719.68-.96c-258.35.44-516.59.96-719.67-.06m-.64-.1c-.82-193.21.25-385.8.45-718.89m-.3 719.61c2.47-171.32 2.89-342.63.09-720.04"></path><text x="540" y="109" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="540" dy="0">sophia</tspan></text><path fill="#142229" d="m578.8 164.06 619.75.67 2.49 121.08-619.93-.42"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M580.11 165.63c123.19.76 247.67 1.27 620.08-1.15m-620.5.59c159.28-.21 318.04-.55 620.32-.06m.43 1c-1.9 24.96-1.6 47.71-1.75 118.56m1.88-120.16c.27 27.76.55 52.9-.69 119.82m-.43.55c-240.71 2.25-479.94 2.84-619.44.68m619.86-.75c-192.68 1.28-385.23 1.58-619.76.09m1.24-1.18c.13-27.97-.66-56.47-.25-119.09m-1.48 121.45c-.39-45.65-.32-88.43.67-120.12"></path><text x="605" y="188" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="605" dy="0">gliderlabs/ssh server</tspan></text><text x="605" y="219" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">listens on :2222</tspan></text><text x="605" y="244" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">host key from --ssh-private-key</tspan></text><path fill="#142229" d="m581.99 323.43 616.92-.21.16 120.46-618.56 1.97"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M579.66 325.51c124.24 2.34 247.97 1.6 619.59.23m-619.24-.62c140.67.5 281.81.74 619.75-.1m.97-1.01c-1.03 41.6.8 86.19 1.08 121.6m-.86-121.6c-1.95 44.94-1.48 88.84-1.81 121.03m.48.65c-194.52.51-388.17.96-620.13.08m620.56-.72c-197.67-1.7-394.31-1.47-620.14-.12m-.32.44c.21-41.28 1.56-79.45 2.25-118.49m-2.48 119.08c.96-34.08 1.08-66.05.45-120.2"></path><text x="605" y="348" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="605" dy="0">bash shell</tspan></text><text x="605" y="379" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">mvdan.cc/sh/v3 interpreter</tspan></text><text x="605" y="404" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">interactive REPL, per-command timeout</tspan></text><path fill="#142229" d="m581.99 485.65 619.38-.32.24 120.69-619.84-1.05"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M579.89 484.57c124.36 3.07 248.76 1.95 619.39 1.14m-619.46-.33c149.4-.5 298.31-.13 620.02-.55m.26 1.69c-2.15 36.4-1.41 77.27-.38 118.4m.21-120.9c.86 30.4 1.57 61.25-.72 120.55m1.42.27c-167.77 1.92-335.85 1.8-620.19.92m619.84-.48c-172.11 1.16-343.88 1.1-620.61-.18m.71 1.46c-1.48-25.92-1.46-51.19-.63-119.74m-.21 119.13c.84-39.13 2.02-76.08.68-120.31"></path><text x="605" y="508" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="605" dy="0">command registry</tspan></text><text x="605" y="539" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">coreutils + wasmprog</tspan></text><text x="605" y="564" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">Python and qjs run as WASI guests</tspan></text><path fill="#142229" d="m581.99 643.86 617.83-.42.33 100.92-621.13-.06"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M580.12 645.22c124.48 2.21 247.94 2.31 619.18.46m-619.68-.85c158.14-.68 315.62-.19 620.29-.18m-.44.37c3.02 29.2 2.68 60.37 2.15 99.21m-.71-100.2c-.64 35.15-1.7 68.23-1.63 100.06m.76 1.49c-141.03.15-281.93 1.05-620.26.16m619.93-1.04c-146.55-.92-292.64-.47-619.49.57m.96-1.53c-.78-25.11-2.08-52.01.49-96.98m-1.95 99.17c.64-35.15.88-70.08.91-100.41"></path><text x="605" y="668" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="605" dy="0">s3fs</tspan></text><text x="605" y="699" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="605" dy="0">rooted at the session bucket</tspan></text><path fill="none" stroke="#1e3340" stroke-dasharray="12 8" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1320.4 94.16c90.78 2.78 184.74 2.46 458.82 1.59m-458.96-.64c123.29-1.51 245.61-1.74 459.73.19m-.46-.89c.6 198.48.03 396.74.28 720m.14-719.8c1.82 158.63 2.29 317.03-.21 720.63m-.37-.55c-175.13 1.47-351.45 1.85-458.52 1.16m459.07-.94c-181.63 1.73-363.53 1.93-459.94-.33m.81.39c1.68-244.47.63-490.64-.95-719.29m.12 719.68c.81-279.82.53-559.54-.35-720.2"></path><text x="1340" y="109" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1340" dy="0">Tigris</tspan></text><path fill="#142229" d="m1358.78 214.92 381.59.25-1.18 118-381.06 3.52"></path><path fill="none" stroke="#2a4050" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1359.15 215.07c77.76.07 153.55 1 381.29-1.03m-380.88 1.42c118.26.38 235.96.61 380.93-.37m1.42-1.06c-1.58 40.16-1.62 76.08-3.6 121.76m2.17-121.35c-1.65 43.62-.43 86.56-.32 119.88m.29.02c-82.39 1.64-164.75 1.49-379.55 1.27m379.07-.89c-131.74 2.88-263.8 2.81-380.07-.21m.63-.15c-1.86-38.53.06-75.38.24-120.08m-.44 121.63c-.86-37.82-.54-76.54-.41-120.53"></path><text x="1390" y="238" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="1390" dy="0">base bucket</tspan></text><text x="1390" y="269" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1390" dy="0">$BUCKET_NAME</tspan></text><text x="1390" y="294" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1390" dy="0">long-lived template</tspan></text><path fill="#142229" d="m1361.98 494.3 378.75-.64.49 131.38-379.69 1.91"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1360.86 494.9c74.53 1.89 151.58.93 378.17 1.02m-378.98-.39c106.39-2.7 213.71-2.8 380.06-.43m-1.91.93c.82 31.3 2.31 63.56 3.23 130.81m-.57-132.8c-1.48 39.84-.07 80.57-1.45 131.09m1.29.59c-130.14-.86-258.77 1.17-380.59.35m380.13-.84c-135.21-.03-269.4-.21-380.64-.52m-.84 1.4c2.18-29.93 2.23-54.54 2.75-129.47m-1.43 129.25c.49-30.5.85-58.67-.64-130.61"></path><text x="1390" y="518" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="1390" dy="0">session bucket</tspan></text><text x="1390" y="549" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1390" dy="0">${BUCKET_NAME}-&lt;uuidv7&gt;</tspan></text><text x="1390" y="579" fill="#CF8E5B" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1390" dy="0">ephemeral, per connection</tspan></text><path fill="none" stroke="#CF5B5B" stroke-dasharray="12 8" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1360.52 674.81c76.51.52 150.59-.18 379.01-.34m-379.84.49c139.06-1.95 279.18-1.03 380.22-.45m.44-1.47c-.63 37.91-.37 73.38-1.35 110.32m1.05-108.51c-.5 41.61-.99 83.91-1.01 109.69m1.79.66c-78.02 1.5-156.19-.17-381.11-.1m380.65-.22c-98.94.79-196.79 1.17-380.91-.07m.42.18c-.58-38.94.54-76.75 0-109.18m1.07 110.08c-2.01-32.66-1.62-63.35-1.74-110.66"></path><text x="1420" y="694" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1420" dy="0">on disconnect:</tspan></text><text x="1420" y="719" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1420" dy="0">session bucket is force-deleted</tspan></text><text x="1420" y="743" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="13" transform="translate(0 15)"><tspan x="1420" dy="0">(Tigris-Force-Delete, 1m timeout)</tspan></text><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M288.01 381.47c30.93-16.85 60.15-31.41 150.44-80.01m-148.86 78.06c45.15-21.96 88.63-45.41 150.78-79.82m1.2-.16c28.37-17.66 60.3-35.52 137.77-73.39m-139.5 72.9c55.1-26.83 109.03-55.78 139.64-74.39"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M571.89 239.14c1.53-4.11 2.47-6.56 7.43-13.62m-6.68 12.21c1.02-3.45 2.83-5.51 7.61-12"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M579.8 226.4c-3.08-1.77-6.74-1.77-15.48-1.35m16.23-.06c-4.22-.54-7.68.21-15.29.27"></path><text x="340" y="264" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="340" dy="0">SSH on :2222</tspan></text><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1199.72 234.4c31.08 9.01 65.83 14.7 161.14 28.85m-160.56-28.2c54.48 9.52 107.04 20.63 158.91 29.15M1346.94 269.82c.99-2.27 5.82-1.51 14.19-3.71m-15.71 4.25c4.74-.92 8.05-2.26 13.96-5"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1361.09 264.84c-4.15-3.27-4.44-3.47-11.35-8.49m9.83 9.03c-2.21-2.41-5.86-5.06-11.57-9.78"></path><text x="1230" y="192" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1230" dy="0">CreateBucketFork</tspan></text><path fill="none" stroke="#5BA4CF" stroke-dasharray="12 8" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1551.44 346.33c-3.68 26.79-1.47 56.73-2.17 137.7m1.73-138.45c-1.33 51.84-1.46 104.89-.95 140.13"></path><path fill="none" stroke="#5BA4CF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1541.87 473.3c.98 1.59 4.12 4.62 8.07 10.4m-7.34-12.41c1.77 5.24 4.56 8.5 7.41 13.7"></path><path fill="none" stroke="#5BA4CF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1549.37 486.29c.83-3.62 3.97-5.81 8.07-15.58m-7.34 13.57c1.96-2.95 4.75-7.88 7.41-12.28"></path><text x="1570" y="393" fill="#8fa3b0" font-family="Virgil, Segoe UI Emoji, cursive" font-size="13" transform="translate(0 15)"><tspan x="1570" dy="0">copy-on-write fork</tspan></text><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1199.24 679.33c34.42-25.39 64.44-49.33 162.28-119.49M1199.73 679.2c36.65-24.9 71.53-52.79 161.07-119.01"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1354.77 573.53c1.68-3.51 1.14-4.2 3.98-14.24m-4.02 14.18c1.63-4.51 3.64-9.65 5.91-13.85"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1360.66 559.73c-2.34-.23-7.06 2.21-16.81 1.35m16.78-1.4c-5.69 1.27-11.12 1.71-14.88 1.74M1358.23 583.42c-30.73 28.45-61.46 52.02-159.21 129.99m161.03-128.08c-38.76 30.91-77.09 62.76-160.66 129.64"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1205.15 699.94c-1.45 5.27-1.43 7.99-5.5 16.14m5.09-14.47c-1.64 4.73-4.04 9.42-5.28 12.83"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1199.8 713.96c2.75 1.99 6.81 1.43 14.66-.25m-15.07 1.92c6.07-1.57 11.32-3.1 14.88-3.56"></path><text x="1220" y="619" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="14" transform="translate(0 15)"><tspan x="1220" dy="0">read / write</tspan></text><path fill="#62FEB5" d="M441.16 284.63c3.13-.29 7.48 1.82 9.78 4.03 2.31 2.21 3.46 5.88 4.05 9.23.58 3.35.99 7.82-.54 10.88-1.52 3.06-5.56 6.17-8.61 7.5s-6.82 1.51-9.69.5-5.84-3.53-7.53-6.56-2.89-8.07-2.62-11.61c.28-3.54 1.46-7.45 4.26-9.63 2.79-2.19 10.27-2.73 12.51-3.49 2.24-.75.92-1.22.92-1.06m-1.13-.74c2.63-.01 4.93 2.92 7.18 5.46 2.25 2.55 5.44 6.27 6.32 9.79.88 3.53.65 8.59-1.05 11.36-1.69 2.77-5.63 4.34-9.11 5.24-3.48.91-8.65 1.2-11.77.18s-5.78-3.34-6.96-6.3-.98-8.06-.1-11.46c.87-3.41 2.9-6.7 5.37-8.96 2.48-2.26 8.16-4.03 9.47-4.61 1.32-.58-1.5.82-1.59 1.12"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M438.24 284.71c2.82-.75 6.17.56 8.98 2.33 2.81 1.78 6.58 5.33 7.88 8.33 1.31 2.99.91 6.35-.04 9.63-.96 3.27-3.02 7.98-5.67 10.02-2.65 2.05-7.04 2.62-10.22 2.24-3.17-.37-6.46-1.76-8.81-4.5-2.36-2.75-5.09-8.22-5.32-11.97s1.61-7.69 3.92-10.52c2.31-2.84 8.07-5.65 9.95-6.5s1.26 1.07 1.33 1.39m5.69-.26c3.15.66 7.38 2.8 9.16 5.91 1.78 3.1 2.1 9.19 1.52 12.72-.59 3.53-2.42 6.34-5.03 8.46-2.62 2.12-7.24 3.94-10.68 4.25-3.43.31-7.49-.2-9.91-2.38-2.43-2.18-4.18-7.34-4.64-10.71s.59-6.53 1.9-9.52c1.3-2.98 3.14-7.14 5.93-8.41 2.78-1.27 8.67.77 10.78.77s1.84-.96 1.89-.77"></path><text x="437" y="293" fill="#0A171E" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="437" dy="0">1</tspan></text><path fill="#62FEB5" d="M1279.81 212.33c3.03-.58 7.43.99 10.15 2.98s5.44 5.64 6.17 8.97-.5 7.67-1.8 11.02c-1.3 3.36-3.1 7.39-6 9.11s-7.98 2.23-11.37 1.22c-3.4-1.01-7.04-4.19-9.01-7.26-1.97-3.06-3.16-7.76-2.83-11.12.34-3.36 2.16-6.44 4.82-9.04s9.41-5.77 11.16-6.56-.4 1.58-.67 1.81m.51-2.34c3.1.15 6.3 3.56 9.04 6.04 2.75 2.48 6.96 5.25 7.44 8.83.47 3.58-2.68 9.54-4.58 12.62-1.89 3.09-3.98 4.96-6.8 5.88-2.83.92-7.31.74-10.16-.36-2.86-1.09-5.56-3.05-6.95-6.21-1.39-3.17-1.63-9.22-1.39-12.76.25-3.55.7-6.54 2.87-8.5s8.37-2.45 10.17-3.25.75-1.71.63-1.55"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1284.94 212.87c3.13.4 7 2.6 8.96 5.58 1.95 2.97 3.04 8.56 2.76 12.25-.29 3.7-2.22 7.6-4.49 9.93-2.26 2.34-5.76 3.73-9.12 4.09-3.35.37-8.07.1-11.02-1.89s-6.01-6.46-6.7-10.05c-.68-3.59 1.22-8.27 2.61-11.48 1.39-3.2 2.6-6.35 5.74-7.77 3.15-1.42 10.66-.86 13.13-.74 2.47.11 1.73 1.1 1.7 1.43m-6.2-1.16c3.11-.1 5.16.05 7.65 2.36s6.41 7.99 7.3 11.48c.89 3.48-.21 6.48-1.98 9.43-1.76 2.95-5.32 6.78-8.62 8.28s-8.06 1.9-11.17.72c-3.12-1.18-6.01-4.38-7.52-7.8-1.52-3.41-1.88-9.09-1.55-12.67s.86-6.78 3.51-8.8c2.65-2.03 10.39-2.61 12.37-3.37 1.97-.77-.21-1.54-.52-1.24"></path><text x="1277" y="221" fill="#0A171E" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="1277" dy="0">2</tspan></text><path fill="#62FEB5" d="M1278.48 640.72c3.22-.78 7.64.13 10.42 1.72 2.78 1.58 5.06 4.83 6.26 7.79s2.01 6.69.94 9.99c-1.06 3.3-4.28 7.89-7.33 9.82-3.06 1.93-7.75 2.35-10.98 1.76-3.23-.6-6.52-2.87-8.42-5.35-1.91-2.48-3.06-6.04-3.01-9.52.06-3.48.71-8.59 3.35-11.35 2.63-2.75 9.99-4.51 12.46-5.17s2.59.93 2.33 1.22m-2.17-2.46c3.06-.18 5.65 4.04 7.89 6.23s4.98 3.64 5.55 6.93c.58 3.29-.5 9.71-2.1 12.81s-4.32 4.97-7.5 5.79c-3.17.82-8.47.17-11.55-.88-3.07-1.05-5.32-2.86-6.92-5.43-1.6-2.56-3.29-6.63-2.68-9.97.62-3.35 3.49-7.65 6.36-10.09 2.87-2.43 9.09-4.05 10.87-4.51 1.79-.47 0 1.25-.16 1.74"></path><path fill="none" stroke="#62FEB5" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1276.59 641.42c2.96-1.06 7.36-1.22 10.55.1s7.3 4.66 8.6 7.83.33 8-.8 11.21c-1.14 3.2-3.38 6.18-6.04 8.02-2.66 1.85-6.73 3.29-9.93 3.04-3.19-.24-7.11-2.06-9.26-4.5s-3.47-6.82-3.62-10.13c-.16-3.3.85-6.97 2.7-9.72s6.96-5.88 8.39-6.79.03.99.2 1.34m-1.69-1.52c2.78-1.26 8.26-.35 11.23 1.28 2.98 1.63 5.3 5.72 6.61 8.52s2.02 5.1 1.25 8.29c-.76 3.18-3.46 8.39-5.84 10.8-2.39 2.4-5.44 4.13-8.46 3.62s-7.26-4.27-9.68-6.68c-2.42-2.42-4.63-4.79-4.83-7.81-.21-3.01 1.83-7.45 3.6-10.27 1.78-2.81 5.87-5.71 7.06-6.6 1.18-.89.24.88.07 1.25"></path><text x="1277" y="648" fill="#0A171E" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="1277" dy="0">3</tspan></text><path fill="#CF5B5B" d="M1386.16 693.65c3.08-1.03 7.81-.68 10.63.67s4.96 4.19 6.29 7.42 2.51 8.76 1.68 11.97c-.82 3.2-4.1 5.33-6.62 7.28s-5.45 4.67-8.49 4.41c-3.05-.26-7.28-3.5-9.77-5.98-2.49-2.47-4.61-5.64-5.16-8.88-.55-3.25-.63-7.77 1.85-10.58 2.47-2.82 10.37-5.25 13.01-6.32s3.03-.49 2.84-.13m-2.71 1.1c3.03-.51 7.02-1.32 9.75.81 2.74 2.13 6 8.63 6.67 11.97s-.82 5.16-2.63 8.07c-1.8 2.92-5.17 8.09-8.2 9.42-3.02 1.34-6.62-.29-9.93-1.42-3.3-1.12-8.09-2.57-9.9-5.34-1.82-2.77-1.95-7.79-.96-11.27.98-3.48 4.28-7.49 6.85-9.61 2.57-2.13 7.29-2.47 8.56-3.15s-.75-1.28-.94-.94"></path><path fill="none" stroke="#CF5B5B" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1394.28 694.29c3.34.66 7 2.62 8.71 5.09s1.84 6.51 1.56 9.76c-.29 3.25-1.12 7.04-3.27 9.74-2.15 2.71-6.53 5.87-9.62 6.49s-6.22-.64-8.92-2.74c-2.7-2.11-6.11-6.62-7.28-9.9s-.88-6.92.27-9.76c1.14-2.85 3.48-5.84 6.61-7.32 3.14-1.47 9.67-1.5 12.21-1.52 2.53-.03 2.95 1.01 3.01 1.36m-1.58-.7c3.18 1.16 7.01 4.71 8.43 7.41 1.43 2.7 1.05 5.75.14 8.81-.91 3.05-3.04 7.41-5.59 9.51-2.54 2.1-6.6 3.24-9.7 3.1-3.09-.13-6.49-1.86-8.86-3.9s-4.79-4.95-5.37-8.33c-.57-3.37 0-9.17 1.91-11.92 1.92-2.74 6.61-3.72 9.6-4.55 2.98-.84 6.88-.5 8.33-.45s.14.31.36.77"></path><text x="1386" y="701" fill="#c8d6de" font-family="Virgil, Segoe UI Emoji, cursive" font-size="18" transform="translate(0 15)"><tspan x="1386" dy="0">4</tspan></text></svg></button><dialog class="dialog_wQla" aria-label="Enlarged diagram"></dialog>
<p>When you ssh in, Sophia mints a UUIDv7 for your session and forks the bucket
named in <code>$BUCKET_NAME</code>. That fork becomes your world. Every <code>ls</code>, <code>cat</code>, and
<code>python3</code> script points at an filesystem rooted in the session bucket. The
moment you disconnect, Sophia hits that bucket with a force-delete and a
one-minute timeout. Whatever you did is gone.</p>
<p>Any username works at the login prompt because Sophia doesn’t authenticate
anyone. There’s nothing to protect; every session is its own pocket universe
with no shared state and no path back to the host filesystem. The only
persistent thing on disk is the SSH host key, which keeps reconnects from
triggering the dreaded <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code> warning in your
SSH client. My production setup puts the SSH keys in a Kubernetes secret, but
there's plenty of ways to do this.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="like-comment-subscribe">Like, comment, subscribe<a href="https://www.tigrisdata.com/blog/agent-sandbox-go/#like-comment-subscribe" class="hash-link" aria-label="Direct link to Like, comment, subscribe" title="Direct link to Like, comment, subscribe" translate="no">​</a></h2>
<p>The version of this post on my blog has much more detail about how Kefka
actually works under the hood and the tradeoffs I made to get there, you can
check it out here:
<a href="https://xeiaso.net/blog/2026/dancing-mad-sandboxing/" target="_blank" rel="noopener noreferrer" class="">Dancing mad with sandboxing</a>.
It also covers the non-AI reasons you'd want to use something like this too.</p>
<p>Also if you use JavaScript or TypeScript for your agents and want something like
this, feel free to check out
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>!
It’s the same bat-action just on a different bat-channel.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Want to give your agents their own little reality to break?</span><p>Tigris bucket forks give every agent invocation an isolated, copy-on-write workspace. The agent does what it does; the source bucket stays clean.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/ai/agent-shell/" class="cta-link"><div>Read the Bucket Forking Docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>Engineering</category>
            <category>AI</category>
            <category>Agents</category>
            <category>Python</category>
            <category>Object Storage</category>
        </item>
        <item>
            <title><![CDATA[You wanted more lifecycle rules. They're here.]]></title>
            <link>https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/</link>
            <guid>https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Tigris lifecycle rules now support multiple rules per bucket with prefix filters, so you can mix transitions and expirations across different prefixes.]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-ee63259870bb4fb10eba374d784d134c.webp" class="hero-image" alt="Ty in a Minecraft sorting room watching hoppers route objects between chests labeled Standard, Infrequent Access, and Glacier">
<p>Anyone who's spent a weekend in Minecraft knows the moment. You've got eleven
double chests labeled "STUFF", you can't find your iron, and you finally cave
and build the sorting room. Hoppers feeding chests, item filters routing
cobblestone to the bulk room, lava bucket waiting for the rotten flesh you'll
never use. Once you have it, you wonder how you lived without it.</p>
<p>Buckets without proper lifecycle rules are the same situation. One giant chest.
No hoppers. You're paying to keep your build screenshots from 2022 sitting next
to last night's logs.</p>
<p>Last year, <a class="" href="https://www.tigrisdata.com/blog/lifecycle-rules/">we shipped lifecycle rules</a> for Tigris. One
rule per bucket, one transition or one expiration. They have been incredibly
useful for a lot of users. But we wanted to take it further. Today is that day:
<strong>multiple rules per bucket</strong>, <strong>prefix filters</strong>, transitions and expirations
mixed however you want.</p>
<p>The hoppers are here.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-actually-new">What's actually new<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#whats-actually-new" class="hash-link" aria-label="Direct link to What's actually new" title="Direct link to What's actually new" translate="no">​</a></h2>
<p>Before this update, you got one lifecycle rule per bucket, applied to every
object in it. That's enough to do "move everything older than 30 days to
Archive," but not much more.</p>
<p>Now:</p>
<ul>
<li class="">A bucket can have <strong>multiple lifecycle rules</strong> — up to ten.</li>
<li class="">Each rule can be scoped to a key prefix using <code>Filter.Prefix</code>. Omit the filter
(or pass <code>Filter: {}</code>) to apply the rule to every object in the bucket.</li>
<li class="">Transition and expiration rules can be mixed in the same bucket however you
want.</li>
<li class="">Each rule can have an <code>ID</code> (up to 36 characters) so you can name what it does.</li>
<li class="">A single rule can include both a transition and an expiration, but only one of
each. So <code>Standard → IA → expire</code> fits in one rule, while chaining
<code>Standard → IA → Glacier</code> takes two.</li>
</ul>
<p>Full reference lives in the docs:
<a href="https://www.tigrisdata.com/docs/buckets/object-lifecycle-rules/" target="_blank" rel="noopener noreferrer" class="">Object Lifecycle Rules</a>
and
<a href="https://www.tigrisdata.com/docs/buckets/objects-expiration/" target="_blank" rel="noopener noreferrer" class="">Object Expiration</a>.</p>
<p>Here's the new shape of a single rule:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"logs-to-ia"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"logs/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Transitions"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">30</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token property">"StorageClass"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"STANDARD_IA"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="A card-style diagram of one rule: ID and Enabled status at the top, then three rows — FILTER (folder icon, prefix value, omit to apply to every object), TRANSITION (arrow icon, STANDARD_IA or GLACIER at a Days threshold, no cold-to-warm transitions), and EXPIRATION (X icon, delete at a Days threshold)." src="data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB2aWV3Qm94PSIwIDAgODgwIDQyMCIKICB3aWR0aD0iODgwIgogIGhlaWdodD0iNDIwIgogIHN0eWxlPSJiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCIKPgogIDxzdHlsZT4KICAgIHRleHQgewogICAgICBmb250LWZhbWlseToKICAgICAgICAiSGFua2VuIEdyb3Rlc2siLAogICAgICAgIHN5c3RlbS11aSwKICAgICAgICAtYXBwbGUtc3lzdGVtLAogICAgICAgICJTZWdvZSBVSSIsCiAgICAgICAgSGVsdmV0aWNhLAogICAgICAgIEFyaWFsLAogICAgICAgIHNhbnMtc2VyaWY7CiAgICB9CiAgICAudGl0bGUgewogICAgICBmb250LXNpemU6IDE4cHg7CiAgICAgIGZpbGw6ICNjOGQ2ZGU7CiAgICAgIGZvbnQtd2VpZ2h0OiA3MDA7CiAgICB9CiAgICAuc3VidGl0bGUgewogICAgICBmb250LXNpemU6IDEycHg7CiAgICAgIGZpbGw6ICM4ZmEzYjA7CiAgICB9CiAgICAuZmllbGQtbGFiZWwgewogICAgICBmb250LXNpemU6IDExcHg7CiAgICAgIGZpbGw6ICM4ZmEzYjA7CiAgICAgIGxldHRlci1zcGFjaW5nOiAwLjEyZW07CiAgICAgIGZvbnQtd2VpZ2h0OiA2MDA7CiAgICB9CiAgICAudmFsdWUgewogICAgICBmb250LWZhbWlseTogIkZpcmEgQ29kZSBWRiIsIHVpLW1vbm9zcGFjZSwgbW9ub3NwYWNlOwogICAgICBmb250LXNpemU6IDE0cHg7CiAgICAgIGZpbGw6ICNjOGQ2ZGU7CiAgICB9CiAgICAuY29uc3RyYWludCB7CiAgICAgIGZvbnQtc2l6ZTogMTFweDsKICAgICAgZmlsbDogIzVhNjc3MDsKICAgICAgZm9udC1zdHlsZTogaXRhbGljOwogICAgfQogICAgLnN0YXR1cy1vbiB7CiAgICAgIGZvbnQtc2l6ZTogMTBweDsKICAgICAgZmlsbDogIzYyZmViNTsKICAgICAgZm9udC13ZWlnaHQ6IDcwMDsKICAgICAgbGV0dGVyLXNwYWNpbmc6IDAuMWVtOwogICAgfQogICAgLmZvb3RlciB7CiAgICAgIGZvbnQtc2l6ZTogMTFweDsKICAgICAgZmlsbDogIzhmYTNiMDsKICAgIH0KICAgIC5jYXJkIHsKICAgICAgcng6IDEyOwogICAgICByeTogMTI7CiAgICAgIGZpbGw6ICMxNDIyMjk7CiAgICAgIHN0cm9rZTogIzYyZmViNTsKICAgICAgc3Ryb2tlLXdpZHRoOiAxLjU7CiAgICB9CiAgICAuZGl2aWRlciB7CiAgICAgIHN0cm9rZTogIzJhMzczMTsKICAgICAgc3Ryb2tlLXdpZHRoOiAxOwogICAgfQogICAgLnN0YXR1cy1kb3QgewogICAgICBmaWxsOiAjNjJmZWI1OwogICAgfQogIDwvc3R5bGU+CgogIDx0ZXh0IHg9IjQ0MCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aXRsZSI+CiAgICBBbmF0b215IG9mIGEgcnVsZQogIDwvdGV4dD4KICA8dGV4dCB4PSI0NDAiIHk9IjQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0ic3VidGl0bGUiPgogICAgT25lIGZpbHRlci4gT25lIHRyYW5zaXRpb24uIE9uZSBleHBpcmF0aW9uLiBVcCB0byB0ZW4gb2YgdGhlc2UgcGVyCiAgICBidWNrZXQuCiAgPC90ZXh0PgoKICA8IS0tIENhcmQ6IHk9NzQgdG8geT0zOTQsIHg9NjAgdG8geD04MjAgLS0+CiAgPHJlY3QgeD0iNjAiIHk9Ijc0IiB3aWR0aD0iNzYwIiBoZWlnaHQ9IjMyMCIgY2xhc3M9ImNhcmQiIC8+CgogIDwhLS0gSGVhZGVyIHJvdyAtLT4KICA8dGV4dCB4PSI4MiIgeT0iMTAwIiBjbGFzcz0iZmllbGQtbGFiZWwiPlJVTEU8L3RleHQ+CiAgPHRleHQgeD0iODIiIHk9IjEyNCIgY2xhc3M9InZhbHVlIj4ibG9ncy10aWVyZWQtcmV0ZW50aW9uIjwvdGV4dD4KCiAgPGNpcmNsZSBjeD0iNzgwIiBjeT0iMTAwIiByPSI2IiBjbGFzcz0ic3RhdHVzLWRvdCIgLz4KICA8dGV4dCB4PSI3NjYiIHk9IjEwNCIgdGV4dC1hbmNob3I9ImVuZCIgY2xhc3M9InN0YXR1cy1vbiI+RU5BQkxFRDwvdGV4dD4KCiAgPGxpbmUgeDE9IjgwIiB5MT0iMTQ0IiB4Mj0iODAwIiB5Mj0iMTQ0IiBjbGFzcz0iZGl2aWRlciIgLz4KCiAgPCEtLSBGaWx0ZXIgcm93OiAxNDgtMjE2IC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgyLCAxNjIpIj4KICAgIDwhLS0gZm9sZGVyIGljb24gLS0+CiAgICA8cGF0aAogICAgICBkPSJNIDAgNCBMIDEwIDQgTCAxMyAwIEwgMzAgMCBMIDMwIDIyIEwgMCAyMiBaIgogICAgICBmaWxsPSIjNWJhNGNmIgogICAgICBvcGFjaXR5PSIwLjg1IgogICAgLz4KICA8L2c+CiAgPHRleHQgeD0iMTMwIiB5PSIxNzIiIGNsYXNzPSJmaWVsZC1sYWJlbCI+RklMVEVSPC90ZXh0PgogIDx0ZXh0IHg9IjEzMCIgeT0iMTk0IiBjbGFzcz0idmFsdWUiPgogICAgPHRzcGFuIGZpbGw9IiM1YmE0Y2YiPnByZWZpeDwvdHNwYW4+OiAibG9ncy8iCiAgPC90ZXh0PgogIDx0ZXh0IHg9IjEzMCIgeT0iMjEyIiBjbGFzcz0iY29uc3RyYWludCI+CiAgICBPbWl0IHRoZSBmaWx0ZXIgdG8gYXBwbHkgdGhlIHJ1bGUgdG8gZXZlcnkgb2JqZWN0IGluIHRoZSBidWNrZXQuCiAgPC90ZXh0PgoKICA8bGluZSB4MT0iODAiIHkxPSIyMjIiIHgyPSI4MDAiIHkyPSIyMjIiIGNsYXNzPSJkaXZpZGVyIiAvPgoKICA8IS0tIFRyYW5zaXRpb24gcm93OiAyMjYtMzEyIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgyLCAyNDQpIj4KICAgIDwhLS0gY2hldnJvbiBhcnJvdyBpY29uIC0tPgogICAgPHBhdGgKICAgICAgZD0iTSAwIDggTCAyMCA4IEwgMjAgMCBMIDM0IDEyIEwgMjAgMjQgTCAyMCAxNiBMIDAgMTYgWiIKICAgICAgZmlsbD0iIzYyZmViNSIKICAgICAgb3BhY2l0eT0iMC45IgogICAgLz4KICA8L2c+CiAgPHRleHQgeD0iMTMwIiB5PSIyNTQiIGNsYXNzPSJmaWVsZC1sYWJlbCI+VFJBTlNJVElPTjwvdGV4dD4KICA8dGV4dCB4PSIxMzAiIHk9IjI3NiIgY2xhc3M9InZhbHVlIj4KICAgIDx0c3BhbiBmaWxsPSIjNjJmZWI1Ij5TVEFOREFSRF9JQTwvdHNwYW4+IGF0IGRheQogICAgPHRzcGFuIGZpbGw9IiNjZjhlNWIiPjMwPC90c3Bhbj4KICA8L3RleHQ+CiAgPHRleHQgeD0iMTMwIiB5PSIyOTQiIGNsYXNzPSJjb25zdHJhaW50Ij4KICAgIE9uZSBwZXIgcnVsZS4gU3RvcmFnZUNsYXNzOiBTVEFOREFSRF9JQSBvciBHTEFDSUVSLgogIDwvdGV4dD4KICA8dGV4dCB4PSIxMzAiIHk9IjMxMCIgY2xhc3M9ImNvbnN0cmFpbnQiPgogICAgQ29sZCB0aWVycyBkb24ndCBnbyBiYWNrIHRvIHdhcm0uCiAgPC90ZXh0PgoKICA8bGluZSB4MT0iODAiIHkxPSIzMTgiIHgyPSI4MDAiIHkyPSIzMTgiIGNsYXNzPSJkaXZpZGVyIiAvPgoKICA8IS0tIEV4cGlyYXRpb24gcm93OiAzMjItMzc2IC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgyLCAzMzYpIj4KICAgIDwhLS0gY3Jvc3NlZCBjaXJjbGUgLS0+CiAgICA8Y2lyY2xlIGN4PSIxNCIgY3k9IjE0IiByPSIxNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjY2Y4ZTViIiBzdHJva2Utd2lkdGg9IjIiIC8+CiAgICA8bGluZSB4MT0iNiIgeTE9IjYiIHgyPSIyMiIgeTI9IjIyIiBzdHJva2U9IiNjZjhlNWIiIHN0cm9rZS13aWR0aD0iMi41IiAvPgogICAgPGxpbmUgeDE9IjIyIiB5MT0iNiIgeDI9IjYiIHkyPSIyMiIgc3Ryb2tlPSIjY2Y4ZTViIiBzdHJva2Utd2lkdGg9IjIuNSIgLz4KICA8L2c+CiAgPHRleHQgeD0iMTMwIiB5PSIzNDgiIGNsYXNzPSJmaWVsZC1sYWJlbCI+RVhQSVJBVElPTjwvdGV4dD4KICA8dGV4dCB4PSIxMzAiIHk9IjM3MCIgY2xhc3M9InZhbHVlIj4KICAgIGRlbGV0ZSBhdCBkYXkgPHRzcGFuIGZpbGw9IiNjZjhlNWIiPjU0MDwvdHNwYW4+CiAgPC90ZXh0PgoKICA8IS0tIEZvb3RlciBiZWxvdyB0aGUgY2FyZCAtLT4KICA8dGV4dCB4PSI0NDAiIHk9IjQxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9ImZvb3RlciI+CiAgICBTY2hlZHVsZXIgcGlja3MgcnVsZXMgdXAgd2l0aGluIG1pbnV0ZXM7IGZpcnN0IGFjdGlvbiBpcyBvbiB0aGUgb2xkZXN0CiAgICBtYXRjaGluZyBvYmplY3QuCiAgPC90ZXh0Pgo8L3N2Zz4K" width="880" height="420" class="img_ev3q"></p>
<p>That's the building block. Let's build three sorting rooms.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sorting-room-1-logs-with-retention-tiers">Sorting room 1: Logs with retention tiers<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#sorting-room-1-logs-with-retention-tiers" class="hash-link" aria-label="Direct link to Sorting room 1: Logs with retention tiers" title="Direct link to Sorting room 1: Logs with retention tiers" translate="no">​</a></h2>
<p>Imagine your service writes structured logs to <code>logs/</code> in a bucket. Engineers
grep them every day for the first month. Quarterly audits pull from them for a
year. After that, nobody touches them. Compliance still says you keep them for
18 months and then they go away.</p>
<p>Before, you'd need a cron job to do this. Or three buckets. Or a lot of luck.</p>
<p>Now you write one rule that does both: a transition and an expiration on the
same prefix.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Rules"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"logs-tiered-retention"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"logs/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Transitions"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">30</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token property">"StorageClass"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"STANDARD_IA"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Expiration"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">540</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>The rule moves anything under <code>logs/</code> to Infrequent Access after 30 days, then
deletes it after 540. Logs older than a month live in cheaper storage. Logs
older than 18 months don't live anywhere.</p>
<p>Exactly what <a class="" href="https://www.tigrisdata.com/blog/lifecycle-rules/">the previous post</a> promised: slowly punting
things down the tier list before they're eventually deleted. Same savings shape
too — older logs at IA cost a fraction of fresh logs at Standard, and at day 540
they cost nothing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sorting-room-2-one-bucket-three-workloads">Sorting room 2: One bucket, three workloads<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#sorting-room-2-one-bucket-three-workloads" class="hash-link" aria-label="Direct link to Sorting room 2: One bucket, three workloads" title="Direct link to Sorting room 2: One bucket, three workloads" translate="no">​</a></h2>
<p>Now picture a SaaS app that stores three kinds of objects in one bucket:</p>
<ul>
<li class=""><code>uploads/</code> — user-generated content. These are forever.</li>
<li class=""><code>thumbnails/</code> — automatically generated from uploads. Regenerable, so they
should expire.</li>
<li class=""><code>exports/</code> — one-time CSV downloads. Once the user has the file, it's dead
weight.</li>
</ul>
<p>Three workloads, three lifecycles, one bucket. With prefix filtering you write
two rules and you're done:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Rules"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"thumbnails-expire"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"thumbnails/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Expiration"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">30</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"exports-expire"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"exports/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Expiration"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">7</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Uploads don't need a rule. They live forever, which is the whole point.</p>
<p>Before this change, you had two options for this kind of thing: run a deletion
script and hope it stays correct, or split your bucket into three. Both were
bad. Now it's two JSON objects in a config file.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sorting-room-3-an-ml-training-pipeline">Sorting room 3: An ML training pipeline<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#sorting-room-3-an-ml-training-pipeline" class="hash-link" aria-label="Direct link to Sorting room 3: An ML training pipeline" title="Direct link to Sorting room 3: An ML training pipeline" translate="no">​</a></h2>
<p>If you're training models on Tigris, your bucket probably looks something like
this:</p>
<ul>
<li class=""><code>datasets/raw/</code> — your source data. Used hard during a training run, barely
after.</li>
<li class=""><code>checkpoints/</code> — model snapshots from each run. Hot during training, cold once
the run is done.</li>
<li class=""><code>artifacts/intermediate/</code> — embeddings, tokenized batches, debug outputs. You
generate them, you regenerate them, and you mostly throw them away.</li>
</ul>
<p>This is where multi-rule lifecycle earns its keep. One bucket holds three
prefixes, each on its own policy:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token property">"Rules"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"raw-archive"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"datasets/raw/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Transitions"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">14</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token property">"StorageClass"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"GLACIER"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"checkpoints-cool"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"checkpoints/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Transitions"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">7</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token property">"StorageClass"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"STANDARD_IA"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"ID"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"intermediate-expire"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Status"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Enabled"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Filter"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Prefix"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"artifacts/intermediate/"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">      </span><span class="token property">"Expiration"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token property">"Days"</span><span class="token operator">:</span><span class="token plain"> </span><span class="token number">3</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Raw datasets get archived two weeks after a training run. Checkpoints cool down
to Infrequent Access after a week. Intermediate artifacts get deleted after
three days.</p>
<p>Zero egress on a globally distributed, multi-cloud bucket helps here. Archiving
training data on other clouds is a decision you think twice about because
thawing it back out costs you. On Tigris, it's storage that costs less.</p>
<p><a class="" href="https://www.tigrisdata.com/blog/lifecycle-rules/">Pt 1's savings example</a>: a 120 TB bucket dropped from
$2,450 to $833 a month after 92 TB moved to Archive. About two thirds off the
original bill. Raw datasets dwarf checkpoints and intermediate artifacts in most
training pipelines, so the bulk of the savings on Tigris comes from the
<code>GLACIER</code> transition on <code>datasets/raw/</code> — the corpus that drives most of the
storage bill in training workloads.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-rules-are-evaluated">How rules are evaluated<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#how-rules-are-evaluated" class="hash-link" aria-label="Direct link to How rules are evaluated" title="Direct link to How rules are evaluated" translate="no">​</a></h2>
<p>Multi-rule lifecycle on Tigris doesn't have a coordination layer in front of the
workers. Each rule runs on its own worker, walking the bucket oldest-first. If
two rules match the same object, whichever worker picks it up first does the
work.</p>
<p>You can't transition from a colder tier to a warmer one. Once an object lands in
<code>GLACIER</code>, no rule can pull it back to <code>STANDARD_IA</code>. When two transitions race,
the one that fires first is the only one that matters — the other has nothing to
do by the time it arrives.</p>
<p>When a transition and an expiration fire on the same object at roughly the same
moment, <a class="" href="https://www.tigrisdata.com/blog/lifecycle-rules/">pt 1's global replication</a> settles it the same
way it settles any other concurrent metadata update: by timestamp. Multi-rule
reuses the architecture pt 1 walked through.</p>
<p><img decoding="async" loading="lazy" alt="A state diagram showing three storage tiers (Standard, Infrequent Access, Glacier) in a row, with green forward transition arrows between them. Orange expiration arrows from each tier converge on a Deleted node below. A dashed red arc curves over the tier row from Glacier back toward Standard, struck through with an X and labeled &amp;quot;NO PATH BACK,&amp;quot; showing that backwards transitions are blocked." src="data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB2aWV3Qm94PSIwIDAgOTAwIDQ2MCIKICB3aWR0aD0iOTAwIgogIGhlaWdodD0iNDYwIgogIHN0eWxlPSJiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCIKPgogIDxzdHlsZT4KICAgIHRleHQgewogICAgICBmb250LWZhbWlseToKICAgICAgICAiSGFua2VuIEdyb3Rlc2siLAogICAgICAgIHN5c3RlbS11aSwKICAgICAgICAtYXBwbGUtc3lzdGVtLAogICAgICAgICJTZWdvZSBVSSIsCiAgICAgICAgSGVsdmV0aWNhLAogICAgICAgIEFyaWFsLAogICAgICAgIHNhbnMtc2VyaWY7CiAgICB9CiAgICAudGl0bGUgewogICAgICBmb250LXNpemU6IDE4cHg7CiAgICAgIGZpbGw6ICNjOGQ2ZGU7CiAgICAgIGZvbnQtd2VpZ2h0OiA3MDA7CiAgICB9CiAgICAuc3VidGl0bGUgewogICAgICBmb250LXNpemU6IDEycHg7CiAgICAgIGZpbGw6ICM4ZmEzYjA7CiAgICB9CiAgICAudGllci1uYW1lIHsKICAgICAgZm9udC1zaXplOiAxNXB4OwogICAgICBmb250LXdlaWdodDogNzAwOwogICAgICBmaWxsOiAjYzhkNmRlOwogICAgfQogICAgLnRpZXItY29kZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAiRmlyYSBDb2RlIFZGIiwgdWktbW9ub3NwYWNlLCBtb25vc3BhY2U7CiAgICAgIGZvbnQtc2l6ZTogMTFweDsKICAgICAgZmlsbDogIzhmYTNiMDsKICAgIH0KICAgIC5hcnJvdy1sYWJlbCB7CiAgICAgIGZvbnQtc2l6ZTogMTBweDsKICAgICAgZmlsbDogIzYyZmViNTsKICAgICAgZm9udC13ZWlnaHQ6IDcwMDsKICAgICAgbGV0dGVyLXNwYWNpbmc6IDAuMDhlbTsKICAgIH0KICAgIC5leHBpcmUtbGFiZWwgewogICAgICBmb250LXNpemU6IDEwcHg7CiAgICAgIGZpbGw6ICNjZjhlNWI7CiAgICAgIGZvbnQtd2VpZ2h0OiA3MDA7CiAgICAgIGxldHRlci1zcGFjaW5nOiAwLjA4ZW07CiAgICB9CiAgICAuYmxvY2tlZC1sYWJlbCB7CiAgICAgIGZvbnQtc2l6ZTogMTFweDsKICAgICAgZmlsbDogI2NmNWI1YjsKICAgICAgZm9udC13ZWlnaHQ6IDcwMDsKICAgICAgbGV0dGVyLXNwYWNpbmc6IDAuMWVtOwogICAgfQogICAgLmZvb3RlciB7CiAgICAgIGZvbnQtc2l6ZTogMTFweDsKICAgICAgZmlsbDogIzhmYTNiMDsKICAgIH0KICAgIC50aWVyLXN0YW5kYXJkIHsKICAgICAgcng6IDEwOwogICAgICByeTogMTA7CiAgICAgIGZpbGw6IHJnYmEoMjA3LCAxNDIsIDkxLCAwLjEyKTsKICAgICAgc3Ryb2tlOiAjY2Y4ZTViOwogICAgICBzdHJva2Utd2lkdGg6IDEuNTsKICAgIH0KICAgIC50aWVyLWlhIHsKICAgICAgcng6IDEwOwogICAgICByeTogMTA7CiAgICAgIGZpbGw6IHJnYmEoOTEsIDE2NCwgMjA3LCAwLjEyKTsKICAgICAgc3Ryb2tlOiAjNWJhNGNmOwogICAgICBzdHJva2Utd2lkdGg6IDEuNTsKICAgIH0KICAgIC50aWVyLWdsYWNpZXIgewogICAgICByeDogMTA7CiAgICAgIHJ5OiAxMDsKICAgICAgZmlsbDogcmdiYSg3NSwgMTEwLCAxNDUsIDAuMik7CiAgICAgIHN0cm9rZTogIzZkOTZiZDsKICAgICAgc3Ryb2tlLXdpZHRoOiAxLjU7CiAgICB9CiAgICAudGllci1kZWxldGVkIHsKICAgICAgcng6IDEwOwogICAgICByeTogMTA7CiAgICAgIGZpbGw6IHRyYW5zcGFyZW50OwogICAgICBzdHJva2U6ICM0YTU1NjA7CiAgICAgIHN0cm9rZS13aWR0aDogMS41OwogICAgICBzdHJva2UtZGFzaGFycmF5OiA1IDQ7CiAgICB9CiAgICAuYXJyb3ctZm9yd2FyZCB7CiAgICAgIHN0cm9rZTogIzYyZmViNTsKICAgICAgc3Ryb2tlLXdpZHRoOiAyLjU7CiAgICAgIGZpbGw6IG5vbmU7CiAgICB9CiAgICAuYXJyb3ctZXhwaXJlIHsKICAgICAgc3Ryb2tlOiAjY2Y4ZTViOwogICAgICBzdHJva2Utd2lkdGg6IDI7CiAgICAgIGZpbGw6IG5vbmU7CiAgICAgIHN0cm9rZS1vcGFjaXR5OiAwLjc1OwogICAgfQogICAgLmFyYy1ibG9ja2VkIHsKICAgICAgc3Ryb2tlOiAjY2Y1YjViOwogICAgICBzdHJva2Utd2lkdGg6IDIuNTsKICAgICAgZmlsbDogbm9uZTsKICAgICAgc3Ryb2tlLWRhc2hhcnJheTogNyA1OwogICAgICBzdHJva2Utb3BhY2l0eTogMC44NTsKICAgIH0KICA8L3N0eWxlPgoKICA8ZGVmcz4KICAgIDxtYXJrZXIKICAgICAgaWQ9ImFoLWZ3ZCIKICAgICAgbWFya2VyV2lkdGg9IjkiCiAgICAgIG1hcmtlckhlaWdodD0iNyIKICAgICAgcmVmWD0iOSIKICAgICAgcmVmWT0iMy41IgogICAgICBvcmllbnQ9ImF1dG8iCiAgICA+CiAgICAgIDxwb2x5Z29uIHBvaW50cz0iMCAwLCA5IDMuNSwgMCA3IiBmaWxsPSIjNjJmZWI1IiAvPgogICAgPC9tYXJrZXI+CiAgICA8bWFya2VyCiAgICAgIGlkPSJhaC1leHAiCiAgICAgIG1hcmtlcldpZHRoPSI4IgogICAgICBtYXJrZXJIZWlnaHQ9IjYiCiAgICAgIHJlZlg9IjgiCiAgICAgIHJlZlk9IjMiCiAgICAgIG9yaWVudD0iYXV0byIKICAgID4KICAgICAgPHBvbHlnb24gcG9pbnRzPSIwIDAsIDggMywgMCA2IiBmaWxsPSIjY2Y4ZTViIiBmaWxsLW9wYWNpdHk9IjAuOSIgLz4KICAgIDwvbWFya2VyPgogIDwvZGVmcz4KCiAgPHRleHQgeD0iNDUwIiB5PSIyOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InRpdGxlIj4KICAgIFdoZXJlIG9iamVjdHMgY2FuIG1vdmUKICA8L3RleHQ+CiAgPHRleHQgeD0iNDUwIiB5PSI0OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InN1YnRpdGxlIj4KICAgIFRyYW5zaXRpb25zIHB1c2ggb2JqZWN0cyB0byBjb2xkZXIgdGllcnMuIEV4cGlyYXRpb25zIGRlbGV0ZSB0aGVtLgogICAgQmFja3dhcmRzIGlzIGJsb2NrZWQuCiAgPC90ZXh0PgoKICA8IS0tIEJsb2NrZWQgYXJjIOKAlCBjdXJ2ZXMgYWJvdmUgdGhlIHRpZXIgcm93IGZyb20gR2xhY2llciBiYWNrIHRvIFN0YW5kYXJkIC0tPgogIDxwYXRoCiAgICBkPSJNIDY1MCAxODUgQyA2MDAgNTAsIDMwMCA1MCwgMjUwIDE4NSIKICAgIGNsYXNzPSJhcmMtYmxvY2tlZCIKICAvPgogIDwhLS0gU3RyaWtlLXRocm91Z2ggWCBhdCB0aGUgcGVhayBvZiB0aGUgYXJjIC0tPgogIDxsaW5lCiAgICB4MT0iNDM4IgogICAgeTE9Ijc2IgogICAgeDI9IjQ2MiIKICAgIHkyPSIxMDAiCiAgICBzdHJva2U9IiNjZjViNWIiCiAgICBzdHJva2Utd2lkdGg9IjMiCiAgLz4KICA8bGluZQogICAgeDE9IjQ2MiIKICAgIHkxPSI3NiIKICAgIHgyPSI0MzgiCiAgICB5Mj0iMTAwIgogICAgc3Ryb2tlPSIjY2Y1YjViIgogICAgc3Ryb2tlLXdpZHRoPSIzIgogIC8+CiAgPHRleHQgeD0iNDUwIiB5PSIxMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJibG9ja2VkLWxhYmVsIj4KICAgIE5PIFBBVEggQkFDSwogIDwvdGV4dD4KCiAgPCEtLSBUaWVyIHJlY3RhbmdsZXMgLS0+CiAgPHJlY3QgeD0iNDAiIHk9IjE4NSIgd2lkdGg9IjIxMCIgaGVpZ2h0PSI4MiIgY2xhc3M9InRpZXItc3RhbmRhcmQiIC8+CiAgPHRleHQgeD0iMTQ1IiB5PSIyMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLW5hbWUiPlN0YW5kYXJkPC90ZXh0PgogIDx0ZXh0IHg9IjE0NSIgeT0iMjM4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idGllci1jb2RlIj4KICAgIChkZWZhdWx0IHRpZXIpCiAgPC90ZXh0PgoKICA8cmVjdCB4PSIzNDUiIHk9IjE4NSIgd2lkdGg9IjIxMCIgaGVpZ2h0PSI4MiIgY2xhc3M9InRpZXItaWEiIC8+CiAgPHRleHQgeD0iNDUwIiB5PSIyMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLW5hbWUiPgogICAgSW5mcmVxdWVudCBBY2Nlc3MKICA8L3RleHQ+CiAgPHRleHQgeD0iNDUwIiB5PSIyMzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLWNvZGUiPgogICAgU3RvcmFnZUNsYXNzOiBTVEFOREFSRF9JQQogIDwvdGV4dD4KCiAgPHJlY3QgeD0iNjUwIiB5PSIxODUiIHdpZHRoPSIyMTAiIGhlaWdodD0iODIiIGNsYXNzPSJ0aWVyLWdsYWNpZXIiIC8+CiAgPHRleHQgeD0iNzU1IiB5PSIyMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLW5hbWUiPkdsYWNpZXI8L3RleHQ+CiAgPHRleHQgeD0iNzU1IiB5PSIyMzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLWNvZGUiPgogICAgU3RvcmFnZUNsYXNzOiBHTEFDSUVSCiAgPC90ZXh0PgoKICA8IS0tIEZvcndhcmQgdHJhbnNpdGlvbiBhcnJvd3MgKGVuZCBzaG9ydCBvZiB0aWVyIGVkZ2VzIHRvIGxlYXZlIGJyZWF0aGluZyByb29tKSAtLT4KICA8cGF0aAogICAgZD0iTSAyNTYgMjI2IEwgMzM5IDIyNiIKICAgIGNsYXNzPSJhcnJvdy1mb3J3YXJkIgogICAgbWFya2VyLWVuZD0idXJsKCNhaC1md2QpIgogIC8+CiAgPHRleHQgeD0iMjk3IiB5PSIyMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJhcnJvdy1sYWJlbCI+CiAgICBUUkFOU0lUSU9OCiAgPC90ZXh0PgoKICA8cGF0aAogICAgZD0iTSA1NjEgMjI2IEwgNjQ0IDIyNiIKICAgIGNsYXNzPSJhcnJvdy1mb3J3YXJkIgogICAgbWFya2VyLWVuZD0idXJsKCNhaC1md2QpIgogIC8+CiAgPHRleHQgeD0iNjAyIiB5PSIyMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJhcnJvdy1sYWJlbCI+CiAgICBUUkFOU0lUSU9OCiAgPC90ZXh0PgoKICA8IS0tIEV4cGlyYXRpb24gYXJyb3dzIGZyb20gZWFjaCB0aWVyIGdvaW5nIGRvd24gdG8gRGVsZXRlZCAtLT4KICA8cGF0aAogICAgZD0iTSAxNDUgMjczIEMgMTQ1IDMxNSwgNDEwIDMyNSwgNDMwIDM1MCIKICAgIGNsYXNzPSJhcnJvdy1leHBpcmUiCiAgICBtYXJrZXItZW5kPSJ1cmwoI2FoLWV4cCkiCiAgLz4KICA8cGF0aAogICAgZD0iTSA0NTAgMjczIEwgNDUwIDM1MCIKICAgIGNsYXNzPSJhcnJvdy1leHBpcmUiCiAgICBtYXJrZXItZW5kPSJ1cmwoI2FoLWV4cCkiCiAgLz4KICA8cGF0aAogICAgZD0iTSA3NTUgMjczIEMgNzU1IDMxNSwgNDkwIDMyNSwgNDcwIDM1MCIKICAgIGNsYXNzPSJhcnJvdy1leHBpcmUiCiAgICBtYXJrZXItZW5kPSJ1cmwoI2FoLWV4cCkiCiAgLz4KICA8dGV4dCB4PSI1MTQiIHk9IjMyMCIgY2xhc3M9ImV4cGlyZS1sYWJlbCI+RVhQSVJBVElPTjwvdGV4dD4KCiAgPCEtLSBEZWxldGVkIG5vZGUgLS0+CiAgPHJlY3QgeD0iMzcwIiB5PSIzNTYiIHdpZHRoPSIxNjAiIGhlaWdodD0iNTYiIGNsYXNzPSJ0aWVyLWRlbGV0ZWQiIC8+CiAgPHRleHQgeD0iNDUwIiB5PSIzODEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0aWVyLW5hbWUiIGZpbGw9IiM4ZmEzYjAiPgogICAgRGVsZXRlZAogIDwvdGV4dD4KICA8dGV4dCB4PSI0NTAiIHk9IjQwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InRpZXItY29kZSI+CiAgICAoZ29uZSkKICA8L3RleHQ+CgogIDwhLS0gRm9vdGVyIC0tPgogIDx0ZXh0IHg9IjQ1MCIgeT0iNDQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0iZm9vdGVyIj4KICAgIEVhY2ggcnVsZSBkZWZpbmVzIG9uZSB0cmFuc2l0aW9uIChvbmUgZ3JlZW4gYXJyb3cpIHBsdXMgb25lIGV4cGlyYXRpb24KICAgIChvbmUgb3JhbmdlIGFycm93KS4KICA8L3RleHQ+Cjwvc3ZnPgo=" width="900" height="460" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-note-on-whats-not-here-yet">A note on what's not here yet<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#a-note-on-whats-not-here-yet" class="hash-link" aria-label="Direct link to A note on what's not here yet" title="Direct link to A note on what's not here yet" translate="no">​</a></h2>
<p>Filtering is by key prefix only. Not by object tags. If you've used S3 lifecycle
rules in anger, you've probably reached for tag-based filters at some point.
"Expire everything tagged <code>temp</code>." "Transition objects tagged <code>cold</code>." We don't
have that yet. Prefix filtering covers most of what people reach for, but if tag
filtering is the missing piece for you, tell us in
<a href="https://community.tigrisdata.com/" target="_blank" rel="noopener noreferrer" class="">our Discord</a>. That feedback shapes what we
build next.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-turn-it-on">How to turn it on<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#how-to-turn-it-on" class="hash-link" aria-label="Direct link to How to turn it on" title="Direct link to How to turn it on" translate="no">​</a></h2>
<p>Same one-liner as before. The JSON file holds more rules now — up to ten per
bucket. That cap is arbitrary; we can raise it if you need more.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">aws s3api put-bucket-lifecycle-configuration \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --bucket my-bucket \</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  --lifecycle-configuration file://lifecycle.json</span><br></div></code></pre></div></div>
<p>If your bucket already has data in it, the new rules apply on the next scan,
oldest-first — no backfill flag, no migration. Expect the first action within a
few minutes, or up to fifteen or twenty if the scheduler just finished a sweep.</p>
<p>If you'd rather click than write JSON, the
<a href="https://console.storage.dev/" target="_blank" rel="noopener noreferrer" class="">Tigris Dashboard</a> has the same controls. The full
configuration reference is in the docs:
<a href="https://www.tigrisdata.com/docs/buckets/object-lifecycle-rules/" target="_blank" rel="noopener noreferrer" class="">Object Lifecycle Rules</a>
covers transitions,
<a href="https://www.tigrisdata.com/docs/buckets/objects-expiration/" target="_blank" rel="noopener noreferrer" class="">Object Expiration</a>
covers deletion, and
<a href="https://www.tigrisdata.com/docs/objects/tiers/" target="_blank" rel="noopener noreferrer" class="">Storage Tiers</a> explains what
<code>STANDARD_IA</code>, <code>GLACIER</code>, and <code>GLACIER_IA</code> actually mean.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrapping-up">Wrapping up<a href="https://www.tigrisdata.com/blog/lifecycle-rules-prefix-filters/#wrapping-up" class="hash-link" aria-label="Direct link to Wrapping up" title="Direct link to Wrapping up" translate="no">​</a></h2>
<p>Every Minecraft player eventually builds the sorting room because the
alternative is chaos. Buckets are no different. Lifecycle rules stop you paying
to store the bits that aren't earning their keep.</p>
<p>We've got more on the way: tag-based filtering, and a few things we're not quite
ready to talk about yet. Keep your eyes peeled.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Set and forget object storage</span><p></p><p>Filter by prefix,<br>more hoppers in your bucket;<br>old bytes drift away.</p><p></p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/get-started/" class="cta-link"><div>Get started today!<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Updates</category>
            <category>object storage</category>
            <category>s3</category>
            <category>feature</category>
            <category>lifecycle</category>
        </item>
        <item>
            <title><![CDATA[How small can we make an interface to Tigris?]]></title>
            <link>https://www.tigrisdata.com/blog/agent-shell-homepage/</link>
            <guid>https://www.tigrisdata.com/blog/agent-shell-homepage/</guid>
            <pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We embedded agent-shell on the Tigris homepage so you can ls, cat, and cd through your buckets like a normal filesystem. Here's how it works.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-fe1e95457e617d49f76ab4ef6b9bc339.webp" class="hero-image" alt="An oil-painted scene of a server rack with blinking lights and tangled cables, installed deep inside a timber-framed mine tunnel.">
<p>I keep wondering how small the interface to object storage can be. Why should
storing files in Tigris have to feel any different than storing them on your
local filesystem? How can we make it as easy to use Tigris as it is to use a
shell?</p>
<p>So obviously, we used
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>
to embed a live demo of Tigris as a filesystem inside your browser and put it
right on the homepage. Interrupt the demo by pressing enter in it, run <code>login</code>,
go through the OAuth flow, and then all your buckets are <em>right there</em>. Use
<code>ls</code>, <code>cat</code>, and <code>cd</code> to poke around in them as if they were normal files on
your laptop, phone, tablet, or Steam Deck.</p>
<!-- -->
<video controls="" width="100%" autoplay="" playsinline="" loop="" muted="" style="margin-bottom:1rem"><source src="/blog/assets/medias/tigris-agent-shell-homepage-demo-c6c29b08da2ba3237a3a1532b0e2958c.mp4" type="video/mp4"></video>
<p>Okay, we don't seriously think that this will become the main way people manage
their buckets. For one the window is a bit too small for a lot of serious
operations, but it makes for a really cool demo. Even though this shell is
limited in terms of what it can do (no python, no compilers, no package
management, etc.), you can do a surprising amount of stuff with it. This kind of
environment is also great for AI agents so that you can constrain their chaos
and prevent them from being able to delete important files in any way that
practically matters.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="so-why-did-you-do-this">So why did you do this?<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#so-why-did-you-do-this" class="hash-link" aria-label="Direct link to So why did you do this?" title="Direct link to So why did you do this?" translate="no">​</a></h2>
<p>Well, why not? It's cool as heck.</p>
<p>It also acts as a great statement of intent: Tigris makes object storage feel
natural. You shouldn't have to learn a totally separate interface to use object
storage. You already know bash, why should object storage be any different?</p>
<p>This also is a great environment for your agents to operate in. Put your agents
inside <a href="https://www.tigrisdata.com/docs/ai/agent-kit/#workspaces" target="_blank" rel="noopener noreferrer" class="">a bucket fork</a>
and then they can't hurt anything but themselves no matter how much they want
to. Everything will be isolated to that bucket fork which you can destroy at the
end of its run. Your sins are cleansed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-is-this-secure">How is this secure?<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#how-is-this-secure" class="hash-link" aria-label="Direct link to How is this secure?" title="Direct link to How is this secure?" translate="no">​</a></h2>
<p>At first glance, this demo seems like it'd be fundamentally insecure or
expensive. Surely we aren't running a <code>bash</code> pod on the server per user
connection with <a href="https://www.tigrisdata.com/docs/training/tigrisfs/" target="_blank" rel="noopener noreferrer" class="">tigrisfs</a>
or something mounting your buckets on the fly, right?</p>
<p>We aren't doing that, it's not a real shell, everything's written in TypeScript
and isolated away from actual filesystems and OS system calls, but in order to
understand the depth of this let's cover the problems involved with isolating
bash sessions from each other.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-young-agents-illustrated-primer-to-isolation">A young agent's illustrated primer to isolation<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#a-young-agents-illustrated-primer-to-isolation" class="hash-link" aria-label="Direct link to A young agent's illustrated primer to isolation" title="Direct link to A young agent's illustrated primer to isolation" translate="no">​</a></h3>
<p>The best way to isolate two bash sessions from each other with a high degree of
certainty that nothing they can do will wreck the other is to put them on
physically separate machines. Then you take those machines and bury them 40 feet
under the ground. Make sure they have no contact with the outside world, not
even power cables because any device authorized by the FCC must accept any
interference that may cause unintended operation.</p>
<p>For obvious reasons, this is unworkable. Not being able to <code>ssh</code> into those
machines means they can't really run shell sessions all that well. Computers
also need power to run and battery backups can only last so long. This is why we
have compromises like virtual machines, microvms, containers, and Linux
namespaces. These bridge the gap between theoretical perfect isolation and
practical concerns about infrastructure spend/complexity.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="its-a-faaaaake">It's a faaaaake<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#its-a-faaaaake" class="hash-link" aria-label="Direct link to It's a faaaaake" title="Direct link to It's a faaaaake" translate="no">​</a></h3>
<p>However, this still assumes that we're using "real" shells with GNU Bash or
similar on the server side. We don't do that. We use
<a href="https://github.com/vercel-labs/just-bash" target="_blank" rel="noopener noreferrer" class="">just-bash</a>, a TypeScript package that
emulates the functionality of a shell without actually needing to execute
commands or touch real filesystems. In practice this is "bash enough" that both
human muscle memory and agent rote recall can use it well enough that the
differences don't matter too much in the real world.</p>
<p>Pedantically, some utilities are missing, others don't have every single flag
that GNU Coreutils does, but it's the same kind of functionality gap you
normally see when using an Alpine Linux container instead of an Ubuntu
container. The important parts are there, and that's all that really matters for
the most part.</p>
<p>Because just-bash is written in TypeScript and uses its own "fake" coreutils
implementation, this means that we can control the universe for our agent shell
a <em>lot</em> more easily than it would be if we implemented this with <code>bash</code> pods on
some server cluster somewhere.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="apis-are-the-lies-we-tell-ourselves-so-we-can-sleep-at-night">APIs are the lies we tell ourselves so we can sleep at night<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#apis-are-the-lies-we-tell-ourselves-so-we-can-sleep-at-night" class="hash-link" aria-label="Direct link to APIs are the lies we tell ourselves so we can sleep at night" title="Direct link to APIs are the lies we tell ourselves so we can sleep at night" translate="no">​</a></h3>
<p>A terrible way to think about OS kernels and their system calls is that running
a process is fundamentally dependency injection. This means that when you start
a <code>bash</code> command on your laptop, the OS kernel is injecting filesystem access,
network access, and other IO actions into your process.</p>
<p>When you start a process in a container, the OS kernel is just injecting
different layers for filesystem and network access. After it does this, it
follows all the same rules that it would for a process outside of a container,
it's just routing them to different places under the hood.</p>
<p>This probably makes a bit more intuitive sense if you've worked with programming
languages like Haskell where you have to explicitly mark when functions have
access to the filesystem, network, or console.</p>
<p>With
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>,
we control the "system call" layer. When <code>cat</code> reads from a file in agent-shell,
it reads from the just-bash representation of a filesystem, which is backed by
Tigris. This results in <code>cat</code> reading a file from the bucket and printing it to
the console. All the guarantees about how everything in the stack works are
maintained and everything quacks enough like it should that the difference in
implementation doesn't matter. The dependencies were injected and everything
works as it should.</p>
<p>As a result, when you use the shell on our homepage, you're not using a "real"
shell in the same way that GNU Bash is a "real" shell, it's a hypothetical
representation of a shell that behaves enough like GNU Bash and GNU Coreutils
that the difference doesn't matter.</p>
<p>Some of you may be asking "Is this really a 'shell' if it's a mocked version
with 'fake' coreutils, a locked down filesystem, and limited network access?"
There are several ways to think about this.</p>
<p>The purist view is that this isn't a "shell" because a "shell" needs to be a
read-eval-print-loop that takes input, breaks it into commands, finds the first
argument of the command on the disk, creates a subprocess for that command,
passes the arguments, waits until it's done, and prints the result to the
screen. That view also says it's not a "shell" because the implementation of
<code>ls</code> and <code>cat</code> aren't stored as program binaries in a bin. I'd ask these purists
if busybox's shell is a shell because it packages its coreutils into the same
binary as the shell, meaning that you can run an entire Linux system with two
files: the kernel and busybox.</p>
<p>The rebel view is that this is a shell because you can type <code>ls</code>, hit enter, and
see the contents of the folder. It doesn't matter that the thing processing the
command isn't a unix program. It doesn't matter that the filesystem is an object
storage bucket. It works enough that your muscle memory is unaffected and you
can just use it normally. Pipes work too, so you can <code>ls | grep whatever</code> and
get a response you can live with.</p>
<p>The nihilist view is that there are no shells anywhere and we just deal with
categories of abstractions that are "shell shaped" enough that we don't notice
the difference between implementations. This view also doesn't believe that APIs
exist because if you bridge the gap well enough, nobody cares because they can
<a href="https://www.digitaltrends.com/computing/nier-automata-steam-deck/" target="_blank" rel="noopener noreferrer" class="">play Windows games on a Linux Steam Deck</a>.
On top of that, filesystems don't exist because they are just theoretical
representations of on-disk data structures that pretend to be "filesystem
enough" that you don't care.</p>
<p>Personally, I think that agent-shell is a shell enough. It's also in a package
called
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>,
so I'd hope it's a shell or we're not being ontologically accurate here. But
really, in my experience it works close enough to being a shell that I'm
comfortable with calling it a shell.</p>
<p>As long as your implementation of APIs matches the stated/unstated behavior of
other implementations of APIs, nobody notices the man behind the curtain. This
principle is responsible for at least 65% of the global economy.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-window-is-too-tiny-how-do-i-make-it-bigger">The window is too tiny, how do I make it bigger?<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#the-window-is-too-tiny-how-do-i-make-it-bigger" class="hash-link" aria-label="Direct link to The window is too tiny, how do I make it bigger?" title="Direct link to The window is too tiny, how do I make it bigger?" translate="no">​</a></h2>
<p>If you want a full screen agent shell experience, head to
<a href="https://tigris.sh/" target="_blank" rel="noopener noreferrer" class="">https://tigris.sh</a> and log in there. You can also run
agent-shell on your computer:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">npx @tigrisdata/agent-shell</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.tigrisdata.com/blog/agent-shell-homepage/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Sometimes a sandbox is too heavy for an agent that only wants a shell, not to
mention a browser. But there are tradeoffs that give you better access to
durable storage and details about how the isolation works.</p>
<p>Why can't we have it all though? Why can't we have something that's lightweight,
isolated, has durable storage, and also safe enough to leave unattended. That's
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class="">@tigrisdata/agent-shell</a>'s
bread and butter.</p>
<p>In order for AI agents to expand out in the future, everything needs to be tiny
and lightweight. That's why shells, even experiments like this one, are
important. The question of "how small can we make the interface" isn't just an
existential exercise of UX code golfing, it's a step towards imagining what this
new agentic architecture looks like where every agent is just a small part of a
much larger system composed of thousands or millions of agents.</p>
<p>At that volume we <em>need</em> lightweight and thin tools but can't pay the cost of
losing persistence or capability. This is the niche we hope agent-shell will
fill in your stack and we want to see what you can invent with it.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Run the shell yourself</span><p>Skip the homepage window — agent-shell runs locally with one command, against your real buckets.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" class="cta-link"><div>NPM 📦<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Engineering</category>
            <category>Updates</category>
            <category>AI</category>
            <category>Agents</category>
            <category>Object Storage</category>
        </item>
        <item>
            <title><![CDATA[Own Your AI Context with Basic Memory]]></title>
            <link>https://www.tigrisdata.com/blog/case-study-basic-memory/</link>
            <guid>https://www.tigrisdata.com/blog/case-study-basic-memory/</guid>
            <pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How Basic Memory uses per-tenant Tigris buckets, rclone bisync, and bucket snapshots to give every user a portable Markdown knowledge base.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/basic-memory-hero-e90e1ee050a9e5bb068a12b18a75132b.webp" class="hero-image" alt="Own your own AI context with Basic Memory — your knowledge, your data, anywhere your AI goes">
<div style="background-color:var(--docs-color-background-100);border:1px solid var(--docs-color-border);border-radius:12px;padding:1.5rem;max-width:42rem;margin:1.5rem auto;position:relative;overflow:hidden"><div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem"><div style="background-color:var(--ifm-color-primary);color:white;border-radius:10px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;flex-shrink:0"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 10 10-12h-9l1-10z" fill="currentColor"></path></svg></div><span style="font-weight:700;font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:var(--docs-color-text)">Quick Summary</span><span style="margin-left:auto;font-size:13px;color:var(--docs-color-text-100);font-weight:500">6 min read</span></div><div style="display:flex;flex-direction:column;gap:1rem"><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Per-tenant isolation.</strong> <span style="color:var(--docs-color-text-100)">Every Basic Memory Cloud user gets their own Tigris bucket with scoped credentials, provisioned automatically at signup.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Markdown as objects.</strong> <span style="color:var(--docs-color-text-100)">Users' Markdown knowledge bases are stored directly as S3 objects on Tigris — the same plain text files they can edit by hand or feed into any model.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Bidirectional sync.</strong> <span style="color:var(--docs-color-text-100)">Rclone-powered sync keeps local and cloud copies in lockstep, with Tigris's strong-consistency header ensuring fresh reads globally.</span></div></div><div style="display:flex;gap:0.75rem;align-items:flex-start"><div style="padding-top:2px"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><circle cx="12" cy="12" r="12" fill="var(--ifm-color-primary)"></circle><path d="M7.5 12.5l3 3 6-6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div><div style="font-size:15px;line-height:1.6"><strong style="color:var(--docs-color-text)">Point-in-time restore.</strong> <span style="color:var(--docs-color-text-100)">Tigris bucket snapshots let users roll back their entire knowledge base to any previous state.</span></div></div></div><div style="position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg, var(--ifm-color-primary), #7c5cfc)"></div></div>
<p>We are the sum of our experiences. All of your notes, your back and forth with
your AI, your history: all of it shapes how AI tools help us work. There's some
element of <em>who you are</em> that's contained in your context. But it's all so
fragile. Switching models or changing platforms, even just opening a new chat,
resets your most valuable context, and you're back to zero.</p>
<p><a href="https://basicmemory.com/" target="_blank" rel="noopener noreferrer" class="">Basic Memory</a> fixes that start-from-zero problem.
Imagine plugging a stenographer into each of your AI interactions, one that
writes down everything you discuss with it, the decisions you make, the
conclusions you reach, and that knows how to call up exactly the details that
matter when you need to refer to them in the future. Users own and control their
own context as a knowledge graph stored in plain Markdown files, no proprietary
formats. You can edit your notes directly via an Apple Notes-esque webapp or you
can use their MCP server directly in your AI tool of choice.</p>
<p>When building their cloud offering, Basic Memory needed a storage backend that
could support their vision technically: strict isolation between tenants,
instant provisioning, and sync that didn't bankrupt the platform. Tigris checked
all these boxes. The bigger thing was that Basic Memory and Tigris believe the
same thing: <strong>you should own your own data, and bring it with you, without
restrictions.</strong></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="knowledge-that-belongs-to-you">Knowledge that belongs to you<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#knowledge-that-belongs-to-you" class="hash-link" aria-label="Direct link to Knowledge that belongs to you" title="Direct link to Knowledge that belongs to you" translate="no">​</a></h2>
<p>Basic Memory started as a Thanksgiving-weekend project in 2024. Paul Hernandez
had spent twenty years writing software for startups and large companies, and he
used Claude and ChatGPT daily. Built-in AI memory was in its earliest infancy.
Chat couldn't possibly save all the information he wanted it to, and Claude
didn't even have a memory at the time. Paul could foresee issues with inevitable
product lock-in once they did. How could he save all the context he wanted? And
how could he switch to a better model without paying the exit tax? When
Anthropic released the
<a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer" class="">Model Context Protocol</a>, the path was
obvious: give every AI assistant access to the same knowledge base through a
single MCP server, stored as plain Markdown.</p>
<p>Conversations with any AI produce structured Markdown containing observations,
tags, and wiki-style links between topics. A local SQLite database indexes
everything for full-text search. The MCP server exposes the knowledge base to
Claude, ChatGPT, Gemini, Cursor, VS Code, and any other MCP-compatible tool.
Both the human and the AI read and write the same files.</p>
<p>Basic Memory's edge over built-in AI memory features is partly the sheer volume
of information it can retain, link together, and call up on command. But,
philosophically, their edge (and virtually their one-word motto) is
transparency. Basic Memory's is a folder of Markdown files you can open in any
text editor. With more than 3,000 GitHub stars and 57,000 downloads per month,
it has found its audience among developers and knowledge workers who want to own
their AI context.</p>
<div class="container_e92R"><div class="content_YZKF"><p>Your knowledge should be yours. AI does what AI does well, but ownership stays
with the human. Switch models tomorrow, and your knowledge comes with you.</p></div><div class="attribution_omEq"><div class="userInfo_Bx4E"><span class="username_bmWP">Paul Hernandez</span><span class="title_dJQq">Founder, Basic Memory</span></div><img src="https://avatars.githubusercontent.com/u/60959?v=4" alt="Paul Hernandez" class="avatar_O8Hd"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-bucket-per-brain">One bucket per brain<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#one-bucket-per-brain" class="hash-link" aria-label="Direct link to One bucket per brain" title="Direct link to One bucket per brain" translate="no">​</a></h2>
<p>The local-first version keeps everything on the user's machine, which is how
users in high-security environments build shared knowledge bases on the
open-source release. In October 2025, the team launched
<a href="https://basicmemory.com/cloud" target="_blank" rel="noopener noreferrer" class="">Basic Memory Cloud</a>, a hosted knowledge base
available across devices, browsers, and AI platforms. The cloud product needed
multi-tenant storage with strict isolation: each user's Markdown files,
organized by project, in a bucket no other user can reach.</p>
<p>Their first build-out was a direct port of the local one. Each tenant got a
dedicated Fly.io server, with their SQLite database and Markdown files on a
per-tenant Fly volume. It worked on day one. It didn't scale. As Paul put it: "a
single server in the cloud, you're just asking for problems." Running N
instances and volumes per user for redundancy spiked the cost.</p>
<p>So they moved the isolation: same servers, different buckets. A small pool of
multi-tenant servers now sits behind a gateway proxy that handles authentication
and routing. Each tenant still gets complete data isolation through their own
Tigris bucket, their own Neon Postgres database, and scoped credentials that can
only reach their data. <strong>Storage is strictly per-tenant; compute is shared.</strong>
The platform runs on a handful of servers instead of N, and per-user cost stops
scaling linearly with users.</p>
<div class="container_e92R"><div class="content_YZKF"><p>Everything is a distributed file system — a giant key-value store in the
cloud. By keeping Markdown files in S3-compatible object storage and indexing
them into Postgres, we get a system that's simple, scalable, and extremely
cost efficient.</p></div><div class="attribution_omEq"><div class="userInfo_Bx4E"><span class="username_bmWP">Paul Hernandez</span><span class="title_dJQq">Founder, Basic Memory</span></div><img src="https://avatars.githubusercontent.com/u/60959?v=4" alt="Paul Hernandez" class="avatar_O8Hd"></div></div>
<p>That architecture only works if per-tenant bucket creation is cheap, fast, and
programmatic. The
<a href="https://www.tigrisdata.com/docs/partners/" target="_blank" rel="noopener noreferrer" class="">Tigris Partner Integration API</a> does
that. A user signs up, <a href="https://polar.sh/" target="_blank" rel="noopener noreferrer" class="">Polar</a> fires a webhook to Basic
Memory, and within 5 to 20 seconds the tenant is fully provisioned: Tigris
bucket, Neon Postgres database, scoped credentials, and all internal
configuration. Users see a brief spinner on a welcome screen, and the platform
is ready.</p>
<p>Each tenant gets their own bucket with credentials that can only reach that
tenant's data. Projects within a knowledge base are organized as prefixes inside
the bucket. There is no shared "tenants" bucket, no row-level security to get
wrong, no custom isolation layer to maintain.</p>
<p>At runtime, the gateway proxy resolves the tenant from the request, attaches the
right credentials and database URL as headers, and the shared API pool reads and
writes against that tenant's bucket and Postgres only. The rclone sync path
skips the API entirely and talks straight to the tenant's bucket with its own
scoped credentials.</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 01</span><span style="font-size:12px;color:#94a3b8">shared compute, per-tenant storage — the bucket is the isolation boundary</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  <span style="color:#64748b">client layer</span></div><div>  ┌────────────┐   ┌───────────────────────────┐              ┌───────────────┐</div><div>  │ web editor │   │ MCP clients               │              │ rclone bisync │</div><div>  └─────┬──────┘   │ (Claude, Cursor, VS Code) │              └───────┬───────┘</div><div>        │          └─────────────┬─────────────┘                      │</div><div>        └───┬────────────────────┘                                    │</div><div>            ▼                                                         │</div><div>  <span style="color:#64748b">shared compute</span>                                                      │</div><div>  ┌─────────────────────────┐                                         │</div><div>  │ gateway proxy           │                                         │ <span style="color:#64748b">direct S3,</span></div><div>  │ auth + tenant lookup    │                                         │ <span style="color:#64748b">skips the API,</span></div><div>  └─────────┬───────────────┘                                         │ <span style="color:#64748b">tenant-scoped</span></div><div>            │  <span style="color:#64748b">X-BM-Tenant-ID</span>                                         │ <span style="color:#64748b">credentials</span></div><div>            │  <span style="color:#64748b">X-BM-Database-URL</span>                                      │</div><div>            │  <span style="color:#64748b">X-BM-Bucket-Name</span>                                       │</div><div>            ▼                                                         │</div><div>  ┌─────────────────────────┐                                         │</div><div>  │ cloud API pool          │                                         │</div><div>  │ (multi-tenant)          │                                         │</div><div>  └─────────┬───────────────┘                                         │</div><div>            ├─────────────────────────┬─────────────────────────┐     │</div><div>            ▼                         ▼                         ▼     ▼</div><div>  ┌────────────────────┐  ┌────────────────────┐  ┌────────────────────┐</div><div>  │ tenant A, isolated │  │ tenant B, isolated │  │ tenant C, isolated │</div><div>  ├────────────────────┤  ├────────────────────┤  ├────────────────────┤</div><div>  │ <span style="color:#4ade80">Tigris bucket A</span>    │  │ <span style="color:#4ade80">Tigris bucket B</span>    │  │ <span style="color:#4ade80">Tigris bucket C</span>    │</div><div>  │ scoped credentials │  │ scoped credentials │  │ scoped credentials │</div><div>  │ Neon Postgres A    │  │ Neon Postgres B    │  │ Neon Postgres C    │</div><div>  └────────────────────┘  └────────────────────┘  └────────────────────┘</div></pre></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sync-that-pays-for-itself">Sync that pays for itself<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#sync-that-pays-for-itself" class="hash-link" aria-label="Direct link to Sync that pays for itself" title="Direct link to Sync that pays for itself" translate="no">​</a></h2>
<p>Users own their files. That's a core design principle, and the cloud product
extends it: edit locally in Obsidian or VS Code and sync to the cloud, or edit
entirely in the cloud via the web editor and sync back down. Any amount of data
can flow either direction. On a traditional cloud that's a cost problem; on
Tigris, Basic Memory ignores how much data leaves their platform.</p>
<p>The sync engine is <code>rclone bisync</code>, pointed at Tigris's S3-compatible endpoint.
Each sync fetches the tenant's scoped credentials from the cloud API, then runs
a bidirectional sync between the local project directory and the corresponding
path in the tenant's Tigris bucket. Markdown files are stored as-is — the same
files you'd edit locally are the same objects sitting in S3.</p>
<p>Tigris is globally distributed by default. A user in Singapore writing from
Cursor hits a nearby Tigris region instead of round-tripping to a single origin.
That latency math is what makes "sync everywhere" feel instant rather than
tolerable. The cloud product would be a noticeably worse product if reads always
came from one region.</p>
<p>Global distribution comes with one tradeoff: edge caches can return slightly
stale data for reads far from the origin. Basic Memory handles this by sending
the <code>X-Tigris-Consistent: true</code> header on every rclone operation, forcing
strongly consistent reads and preventing subtle sync issues. The header is
applied globally rather than per-method because bisync starts with an S3
<code>ListObjectsV2</code> call, which isn't a download or an upload — list-only headers
would miss it. It's a small detail, but it's the kind of thing that turns "works
in demo" into "works in production."</p>
<p>For new objects landing in a tenant's bucket from any path — the rclone sync,
the web editor, or the API —
<a href="https://www.tigrisdata.com/docs/buckets/object-notifications/" target="_blank" rel="noopener noreferrer" class="">Tigris bucket event notifications</a>
tell the cloud API which files changed, so the indexer re-reads only what it
needs. No polling, no scheduled scans.</p>
<div class="mermaid-frame"><figure style="margin:0;display:flex;flex-direction:column;gap:10px;text-align:left"><figcaption style="display:flex;align-items:baseline;gap:14px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"><span style="font-size:11px;color:#475569;letter-spacing:0.14em">FIG 02</span><span style="font-size:12px;color:#94a3b8">how an edit flows through Basic Memory — every path converges at the index</span></figcaption><pre style="margin:0;background:#0f172a;border:1px solid #16202f;border-radius:6px;padding:32px;overflow-x:auto;line-height:1.3;color:#cbd5e1;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;font-size:13px;white-space:pre;text-align:left"><div>  ┌───────────────┐                     ┌───────────────┐               ┌────────────────┐</div><div>  │ local machine │ ◀─────────────────▶ │ <span style="color:#4ade80">Tigris bucket</span> │ ────────────▶ │ cloud API      │</div><div>  └───────────────┘    <span style="color:#64748b">rclone bisync,</span>   │ <span style="color:#4ade80">per-tenant</span>    │  <span style="color:#64748b">bucket-event</span> │ Postgres index │</div><div>                    <span style="color:#64748b">X-Tigris-Consistent</span> └───────────────┘    <span style="color:#64748b">webhook</span>    └─────┬──────────┘</div><div>                                                                              │</div><div>                                                  ┌───────────────────────────┤</div><div>                                                  ▼                           ▼</div><div>                                            ┌────────────┐         ┌────────────────────┐</div><div>                                            │ web editor │         │ remote MCP clients │</div><div>                                            └────────────┘         └────────────────────┘</div></pre></figure></div>
<p>Egress is the load-bearing economic detail. Users sync files back and forth all
day, and agents re-read the same notes while building context. That bill
compounds fast. Zero egress is how a four-person team can offer sync to every
customer at a price they'll pay.</p>
<div class="container_e92R"><div class="content_YZKF"><p>Without zero egress, sync per user is a margin killer. With Tigris, it's the
mandate that makes the platform work.</p></div><div class="attribution_omEq"><div class="userInfo_Bx4E"><span class="username_bmWP">Paul Hernandez</span><span class="title_dJQq">Founder, Basic Memory</span></div><img src="https://avatars.githubusercontent.com/u/60959?v=4" alt="Paul Hernandez" class="avatar_O8Hd"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="file-level-history-for-free">File-level history, for free<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#file-level-history-for-free" class="hash-link" aria-label="Direct link to File-level history, for free" title="Direct link to File-level history, for free" translate="no">​</a></h2>
<p>Basic Memory uses
<a href="https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/" target="_blank" rel="noopener noreferrer" class="">Tigris bucket snapshots</a>
so users can recover to a point-in-time, whether or not they manually take a
snapshot. But the cool part is how Basic Memory used bucket snapshots to build a
more granular feature: file-level diff/merge.</p>
<p>Because every change to the bucket is persisted with a snapshot ID — similar to
a version SHA — Basic Memory can fetch every version of a Markdown file and load
the changes side-by-side in a CodeMirror MergeView. A <code>ListObjectVersions</code> call
returns every version of a single Markdown file, making it easy to inspect and
compare revisions. The user picks which changes to keep, much like resolving a
merge conflict in Git.</p>
<p>As Paul describes, "I had experimented with integrating Git for this and it was
such a chore. This pairs really nicely with our bucket-level snapshot feature —
a user can use snapshots to manage versions for large numbers of files, or use
the file history for a single file."</p>
<video controls="" width="100%"><source src="/blog/img/blog/case-study-basic-memory/basic-memory-file-versions.mp4" type="video/mp4"><p>Download the <a href="https://www.tigrisdata.com/blog/img/blog/case-study-basic-memory/basic-memory-file-versions.mp4">
MP4
</a> version.</p></video>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-tigris-solved-that-other-clouds-didnt">What Tigris solved that other clouds didn't<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#what-tigris-solved-that-other-clouds-didnt" class="hash-link" aria-label="Direct link to What Tigris solved that other clouds didn't" title="Direct link to What Tigris solved that other clouds didn't" translate="no">​</a></h2>
<p>Paul previously worked at the biggest AWS customer in Austin, so he knew what
$bigCloud infrastructure costs in commitment — the kind of complexity that comes
from being too big to question. He dealt with account vending machines for new
tenants, continual quota-increase requests as the user base grew, and the exit
tax of egress fees if you ever wanted to run compute on another provider.</p>
<div class="container_e92R"><div class="content_YZKF"><p>I wanted to build on a modern stack that lets us do right by our users — not
lock them in or charge them more than we have to. What I appreciate about
working with Tigris is that they approach it the same way. They're not
trying to squeeze us on pricing, and having a provider that thinks like
that means we can pass those savings and that trust directly down to our
users.</p><p>Tigris is the good parts of AWS without the bad parts.</p></div><div class="attribution_omEq"><div class="userInfo_Bx4E"><span class="username_bmWP">Paul Hernandez</span><span class="title_dJQq">Founder, Basic Memory</span></div><img src="https://avatars.githubusercontent.com/u/60959?v=4" alt="Paul Hernandez" class="avatar_O8Hd"></div></div>
<p>He wanted to build differently. Tigris fit. Per-tenant buckets, zero egress,
globally distributed reads, bucket-event webhooks, snapshots: each one already
existed as a primitive, not a feature request. The architecture didn't have to
bend around the storage layer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's next<a href="https://www.tigrisdata.com/blog/case-study-basic-memory/#whats-next" class="hash-link" aria-label="Direct link to What's next" title="Direct link to What's next" translate="no">​</a></h2>
<p>Basic Memory recently shipped an updated web editor, improved semantic search,
conversation import from ChatGPT and Claude, and per-project cloud routing —
individual projects can route through the cloud while others stay local. Coming
soon is a <strong>teams product</strong>: organizations where each user keeps their own
bucket but shares an organization-level bucket for common knowledge. New team
members can fork the shared bucket and start with a base set of notes instantly.</p>
<p>Regional bucket placement is on the roadmap too. One open-source user is the
CISO of an EU bank using Basic Memory to manage bank infrastructure processes,
and EU data sovereignty rules dictate exactly where bytes can live. With Tigris,
putting a tenant's bucket in a specific region is a configuration choice, not a
project.</p>
<p>For a four-person team, the less time spent on storage infrastructure, the
better. Tigris handles tenant isolation, sync, snapshots, and global
distribution. Basic Memory spends that runway building something most AI
products refuse to: a memory layer the user actually owns.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Build a multi-tenant product on Tigris</span><p>The Partner Integration API gives every one of your users their own isolated bucket with scoped credentials in seconds — no custom isolation layer to maintain.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://www.tigrisdata.com/docs/partners/" class="cta-link"><div>Read the Partner API Docs<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Customers</category>
            <category>AI</category>
            <category>MCP</category>
            <category>Multi-tenant</category>
            <category>Object Storage</category>
        </item>
        <item>
            <title><![CDATA[Build a Self-Updating Knowledge Base for Under $10]]></title>
            <link>https://www.tigrisdata.com/blog/self-updating-knowledge-base/</link>
            <guid>https://www.tigrisdata.com/blog/self-updating-knowledge-base/</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A self-updating knowledge base on Tigris, maintained overnight by an Anthropic SDK agent loop running through @tigrisdata/agent-shell. Atomic flush on success, discard on throw. The bucket either lands a clean run or stays byte-for-byte unchanged. ~$7 a month, presigned digest URL in Slack each morning.
]]></description>
            <content:encoded><![CDATA[<img src="https://www.tigrisdata.com/blog/assets/images/hero-image-1703c383f064ce635e3b86a5da198479.webp" class="hero-image" alt="A dark IDE panel showing a git-style diff of overnight wiki updates: new pages on binary quantization, an updated vendor release timeline, and a contradiction surfaced on Turbopuffer pricing tiers. A small status block reads 'Flush ✓ · Snapshot v412 · Presigned digest 30d'.">
<p>I track vector search news for a living, and the field ships fast enough that my
browser ended up with 200 unread tabs. Half arXiv papers, half vendor changelogs
and HN threads I half-read on my phone and never came back to. Two weeks ago I
had to write a one-pager on filtered vector search for a partner call and spent
two hours rebuilding context I'd already had: release notes I'd skimmed in
March, a paper from February, an HN thread that disagreed with a vendor's own
pricing page.</p>
<!-- -->
<img src="https://www.tigrisdata.com/blog/assets/images/competition-b27f342926ff62653be88af28afa720b.webp" alt="A close-up of one diff hunk on a wiki vendor page: a competitor flipped binary quantization on behind a feature flag in v1.29 and the wiki picked up the change overnight." style="max-width:42rem;width:100%;margin:1.5rem auto;display:block;border-radius:6px">
<p>So I built <a href="https://github.com/davidmyriel/llm-digest" target="_blank" rel="noopener noreferrer" class=""><code>llm-digest</code></a>, a nightly
GitHub Actions cron that reads my feeds, updates a markdown wiki on a
<a href="https://www.tigrisdata.com/" target="_blank" rel="noopener noreferrer" class="">Tigris</a> bucket, and posts a single digest URL to
Slack. I wake up to the day's reading already done, with notes on what shipped
and which one paper to actually click through.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-karpathy-insight">The Karpathy insight<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#the-karpathy-insight" class="hash-link" aria-label="Direct link to The Karpathy insight" title="Direct link to The Karpathy insight" translate="no">​</a></h2>
<p><a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" target="_blank" rel="noopener noreferrer" class="">Andrej Karpathy's LLM Wiki gist</a>
describes the pattern in a paragraph. You drop raw sources into a <code>sources/</code>
folder, then you point an LLM agent at it with a prompt that tells it how to
maintain a wiki. The agent extracts entities and updates a parallel <code>wiki/</code>
folder of plain markdown, with one page per concept, vendor, paper, or person.
You read the wiki; the agent writes it.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Compile-time, not query-time</div><div class="admonitionContent_BuS1"><p>The difference from RAG: synthesis happens <strong>once, at ingest time</strong>, into a
durable artifact you can read. RAG re-derives the answer on every query against
raw chunks. The wiki gets denser over time. New sources update old pages,
contradictions surface explicitly, and by month three the page on
<code>[[binary-quantization]]</code> is a real reference with provenance back to every
paper that contributed to it.</p></div></div>
<p>The catch in the gist is that ingestion is manual; you do it when you remember
to, which in practice is rarely. My version doesn't wait for me to remember.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="llm-digest-the-wiki-does-the-reading">llm-digest: the wiki does the reading<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#llm-digest-the-wiki-does-the-reading" class="hash-link" aria-label="Direct link to llm-digest: the wiki does the reading" title="Direct link to llm-digest: the wiki does the reading" translate="no">​</a></h2>
<p>A <a href="https://github.com/davidmyriel/llm-digest" target="_blank" rel="noopener noreferrer" class="">GitHub repo</a> holds the schema, the
tool implementations, and a list of RSS feeds, while a Tigris bucket holds the
wiki itself. The scheduling layer is three lines of YAML:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># .github/workflows/daily-ingest.yml</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token key atrule">on</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token key atrule">schedule</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> </span><span class="token key atrule">cron</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"0 7 * * *"</span><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)"># 07:00 UTC daily</span><br></div></code></pre></div></div>
<p>There's no web app, no vector store, no custom backend behind any of this. The
user-facing interface is a URL in Slack and an Obsidian vault on my laptop
synced from the bucket, and the 200 unread tabs are at 47 now and falling.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-in-the-morning-digest">What's in the morning digest<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#whats-in-the-morning-digest" class="hash-link" aria-label="Direct link to What's in the morning digest" title="Direct link to What's in the morning digest" translate="no">​</a></h2>
<p>A single page generated at the end of each ingest run, posted to Slack as a
presigned URL:</p>
<div class="language-markdown codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-markdown codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token title important punctuation" style="color:rgb(248, 248, 242)">#</span><span class="token title important"> 2026-05-04 · Daily digest</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">7 sources ingested · 23 pages updated · 4 created · 1 contradiction.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token bold punctuation" style="color:rgb(248, 248, 242)">**</span><span class="token bold content">Theme:</span><span class="token bold punctuation" style="color:rgb(248, 248, 242)">**</span><span class="token plain"> binary quantization is moving from research to production.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token list punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> [[binary-quantization]] — added 2026 production reports section</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token list punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> [[hnsw]] — a vendor flagged binary quantization in v1.29</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token list punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> [[turbopuffer]] — pricing tier names contradicted by HN thread</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token bold punctuation" style="color:rgb(248, 248, 242)">**</span><span class="token bold content">Recommended:</span><span class="token bold punctuation" style="color:rgb(248, 248, 242)">**</span><span class="token plain"> "BBQ at scale" — clearest single read of the day.</span><br></div></code></pre></div></div>
<p>I click into the one or two pages worth reading and get on with my morning.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-actually-works">How it actually works<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#how-it-actually-works" class="hash-link" aria-label="Direct link to How it actually works" title="Direct link to How it actually works" translate="no">​</a></h2>
<p>The whole thing is a sandwich: the LLM is on top, the Tigris bucket is on the
bottom, and
<a href="https://www.npmjs.com/package/@tigrisdata/agent-shell" target="_blank" rel="noopener noreferrer" class=""><code>@tigrisdata/agent-shell</code></a>
sits in the middle as a JS-virtual filesystem. Every write the agent makes goes
through agent-shell's in-memory buffer; the bucket only sees those writes when
the run cleanly reaches <code>flush()</code> at the end. If anything throws on the way
there, the buffer is discarded and the bucket is byte-for-byte unchanged. The
rollback is a structural property of the runtime, not a pattern I have to
maintain in user code.</p>
<figure class="mermaidFrame_PGZu" style="max-width:42rem;margin:1.5rem auto"><div class="body_AwTr"><svg xmlns="http://www.w3.org/2000/svg" width="720" height="560" aria-label="Flow diagram: Anthropic SDK LLM issues tool_use to read, write, and list helpers, which route through agent-shell's in-memory buffer. On clean return flush() promotes to the Tigris bucket; on throw discard() drops buffered writes and leaves the bucket unchanged." viewBox="0 0 720 560" role="img" style="width:100%;height:auto"><style>text{font-family:"Hanken Grotesk",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif}.title{font-size:18px;font-weight:700;fill:#e2e8f0}.sublabel{font-size:13px;font-weight:500;fill:#94a3b8}.codelabel{font-family:ui-monospace,"JetBrains Mono","Berkeley Mono",monospace;font-size:12px;fill:#a7f3d0}.branch-bad,.branch-good{font-family:ui-monospace,monospace;font-size:13px;font-weight:700;fill:#62feb5}.branch-bad{fill:#e57373}.badge-text,.filename{font-family:ui-monospace,monospace;font-size:10px;fill:#94a3b8}.badge-text{font-size:13px;font-weight:700;fill:#0a0c0e}</style><defs><marker id="head-good" markerHeight="7" markerWidth="9" orient="auto" refX="8.5" refY="3.5"><path fill="#62feb5" d="m0 0 9 3.5L0 7z"></path></marker><marker id="head-bad" markerHeight="7" markerWidth="9" orient="auto" refX="8.5" refY="3.5"><path fill="#e57373" d="m0 0 9 3.5L0 7z"></path></marker><marker id="head-default" markerHeight="7" markerWidth="9" orient="auto" refX="8.5" refY="3.5"><path fill="#94a3b8" d="m0 0 9 3.5L0 7z"></path></marker></defs><rect width="320" height="100" x="200" y="20" fill="#142229" stroke="#334155" stroke-width="2" rx="12" ry="12"></rect><circle cx="222" cy="42" r="14" fill="#62feb5"></circle><text x="222" y="46" class="badge-text" text-anchor="middle">1</text><text x="360" y="48" class="title" text-anchor="middle">The Agent</text><text x="360" y="72" class="sublabel" text-anchor="middle">Anthropic SDK loop, runs in your Node process</text><text x="360" y="98" class="codelabel" text-anchor="middle">writeFile()  ·  readFile()  ·  listDir()</text><path stroke="#94a3b8" stroke-width="2" marker-end="url(#head-default)" d="M360 120v40"></path><text x="370" y="145" style="font-size:12px;font-weight:600;fill:#94a3b8">tool calls</text><rect width="520" height="200" x="100" y="170" fill="#142229" stroke="#62feb5" stroke-width="2.5" rx="12" ry="12"></rect><circle cx="122" cy="192" r="14" fill="#62feb5"></circle><text x="122" y="196" class="badge-text" text-anchor="middle">2</text><text x="360" y="198" fill="#ecfdf5" class="title" text-anchor="middle">agent-shell</text><text x="360" y="222" fill="#a7f3d0" class="sublabel" text-anchor="middle">in-memory buffer — every write stages here, not in the bucket yet</text><g transform="translate(180 250)"><rect width="50" height="60" fill="#1e2c33" stroke="#94a3b8" rx="3"></rect><path stroke="#64748b" d="M6 14h38M6 22h38M6 30h38M6 38h30"></path><text x="25" y="78" class="filename" text-anchor="middle">wiki/binq.md</text></g><g transform="translate(255 250)"><rect width="50" height="60" fill="#1e2c33" stroke="#94a3b8" rx="3"></rect><path stroke="#64748b" d="M6 14h38M6 22h38M6 30h34"></path><text x="25" y="78" class="filename" text-anchor="middle">wiki/hnsw.md</text></g><g transform="translate(330 250)"><rect width="50" height="60" fill="#1e2c33" stroke="#94a3b8" rx="3"></rect><path stroke="#64748b" d="M6 14h38M6 22h38M6 30h38M6 38h26"></path><text x="25" y="78" class="filename" text-anchor="middle">sources/…</text></g><g transform="translate(405 250)"><rect width="50" height="60" fill="#1e2c33" stroke="#94a3b8" rx="3"></rect><path stroke="#64748b" d="M6 14h38M6 22h38"></path><text x="25" y="78" class="filename" text-anchor="middle">digest.md</text></g><path fill="none" stroke="#e57373" stroke-dasharray="6 4" stroke-linecap="round" stroke-width="2.5" marker-end="url(#head-bad)" d="m260 370-60 60"></path><text x="170" y="395" class="branch-bad" text-anchor="end">on throw</text><text x="170" y="412" class="branch-bad" text-anchor="end">discard()</text><path fill="none" stroke="#62feb5" stroke-linecap="round" stroke-width="2.5" marker-end="url(#head-good)" d="m460 370 60 60"></path><text x="550" y="395" class="branch-good">on clean run</text><text x="550" y="412" class="branch-good">flush()</text><rect width="200" height="100" x="100" y="440" fill="#1f1416" stroke="#e57373" stroke-width="2" rx="12" ry="12"></rect><circle cx="122" cy="462" r="14" fill="#e57373"></circle><text x="122" y="466" style="font-family:ui-monospace,monospace;font-size:12px;font-weight:700;fill:#2a0a0a" text-anchor="middle">3a</text><text x="200" y="472" fill="#fda4a4" class="title" text-anchor="middle">buffer dropped</text><text x="200" y="496" fill="#fcb6b6" class="sublabel" text-anchor="middle">the live bucket stays</text><text x="200" y="514" fill="#fcb6b6" class="sublabel" text-anchor="middle">byte-for-byte unchanged</text><rect width="200" height="100" x="420" y="440" fill="#142229" stroke="#62feb5" stroke-width="2.5" rx="12" ry="12"></rect><rect width="168" height="3" x="436" y="452" fill="#62feb5" fill-opacity="0.35" rx="1.5" ry="1.5"></rect><circle cx="442" cy="462" r="14" fill="#62feb5"></circle><text x="442" y="466" class="badge-text" text-anchor="middle">3b</text><text x="520" y="472" fill="#62feb5" class="title" text-anchor="middle">Tigris bucket</text><text x="520" y="496" class="codelabel" text-anchor="middle">wiki/  ·  sources/  ·  state.json</text><text x="520" y="516" class="sublabel" text-anchor="middle">atomic promote, all or nothing</text></svg></div></figure>
<p>The script at the heart of this is small. Mount the bucket via agent-shell, run
a custom <a href="https://docs.anthropic.com/en/api/messages" target="_blank" rel="noopener noreferrer" class="">Anthropic SDK</a> agent loop
with the wiki schema as the system prompt, let the loop call tools that route
every read and write through the shell handle. On clean return, flush. On any
throw, discard.</p>
<div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)">// scripts/lib/shell.ts (the shape that matters)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">function</span><span class="token plain"> </span><span class="token generic-function function" style="color:rgb(80, 250, 123)">withMountedBucket</span><span class="token generic-function generic class-name operator">&lt;</span><span class="token generic-function generic class-name constant" style="color:rgb(189, 147, 249)">T</span><span class="token generic-function generic class-name operator">&gt;</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token function-variable function" style="color:rgb(80, 250, 123)">fn</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">shell</span><span class="token operator">:</span><span class="token plain"> WikiBucketHandle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">=&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">Promise</span><span class="token operator">&lt;</span><span class="token constant" style="color:rgb(189, 147, 249)">T</span><span class="token operator">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">Promise</span><span class="token operator">&lt;</span><span class="token constant" style="color:rgb(189, 147, 249)">T</span><span class="token operator">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> shell </span><span class="token operator">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">mountWikiBucket</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">try</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> result </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">fn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">shell</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> shell</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">flush</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// atomic promote to Tigris</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> result</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">e</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> shell</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token function" style="color:rgb(80, 250, 123)">discard</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// buffer dropped; bucket untouched</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">throw</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div>
<p>Nine lines of fn-and-catch is the entire rollback story. There's no separate
restore primitive and no scratch prefix in the bucket to garbage-collect. The
buffer lives in process memory until flush; if the process exits without
flushing, the buffer is gone with it and the live bucket sees nothing.</p>
<p>The practical consequence: if a runner crashes at 3am, you wake up to
yesterday's wiki, not a half-edited one. The same bucket you went to bed with,
plus a Slack notification telling you the run aborted.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-sdk-and-not-headless-claude-code">Why the SDK and not headless Claude Code<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#why-the-sdk-and-not-headless-claude-code" class="hash-link" aria-label="Direct link to Why the SDK and not headless Claude Code" title="Direct link to Why the SDK and not headless Claude Code" translate="no">​</a></h3>
<p>Claude Code in <code>-p</code> mode runs as a separate child process and writes to the OS
filesystem. agent-shell's JS-virtual buffer can't see those writes, so the
atomicity guarantee only holds if the agent runs in-process. The SDK is the
right runner here for that one reason.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cleaning-up-the-input">Cleaning up the input<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#cleaning-up-the-input" class="hash-link" aria-label="Direct link to Cleaning up the input" title="Direct link to Cleaning up the input" translate="no">​</a></h3>
<p>The agent's <code>fetch_url</code> tool wraps the response body in
<a href="https://github.com/mozilla/readability" target="_blank" rel="noopener noreferrer" class="">Mozilla Readability</a> (the algorithm
Firefox Reader View uses). Strips nav, footer, ads, comment threads. On
real-world URLs from my feeds:</p>
<table><thead><tr><th>URL</th><th style="text-align:right">Raw HTML</th><th style="text-align:right">After Readability</th><th style="text-align:right">Reduction</th></tr></thead><tbody><tr><td>HN discussion page</td><td style="text-align:right">863 t</td><td style="text-align:right">93 t</td><td style="text-align:right">89.2%</td></tr><tr><td>arXiv abstract</td><td style="text-align:right">12,169 t</td><td style="text-align:right">702 t</td><td style="text-align:right">94.2%</td></tr></tbody></table>
<p>Letting Claude eat raw HTML would burn 10–100× more tokens for the same result.
This is the difference between a $5+ run and a $0.23 run; the arithmetic shows
up in "I ran this last night" below.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-tigris-and-agent-shell-earn-their-place">Where Tigris and agent-shell earn their place<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#where-tigris-and-agent-shell-earn-their-place" class="hash-link" aria-label="Direct link to Where Tigris and agent-shell earn their place" title="Direct link to Where Tigris and agent-shell earn their place" translate="no">​</a></h2>
<p>The project follows a rule: don't pitch a tool unless it solves a real problem.</p>
<p><strong>The Tigris bucket is the floor.</strong> A scheduled job needs durable shared storage
that any runner can mount and find the same wiki in. Last week's GitHub Actions
runner, this morning's manual <code>workflow_dispatch</code> from my laptop, the <code>rclone</code>
mount on my phone for read-side browsing: all looking at the same bytes through
the S3-compatible API. Table-stakes work, and Tigris does it without a
Tigris-specific code path.</p>
<p><strong>Snapshots cover the failures agent-shell can't.</strong> agent-shell's flush makes a
single run atomic. It doesn't help when a run "succeeds" but quietly produces a
wiki I don't actually want, where the agent drifted from the schema in a way
that didn't trip a validator. I might not notice for three days.
<a href="https://www.tigrisdata.com/docs/buckets/snapshots/" target="_blank" rel="noopener noreferrer" class=""><code>createBucketSnapshot</code></a>
runs at the top of every ingest, and
<a href="https://www.tigrisdata.com/docs/buckets/forking/" target="_blank" rel="noopener noreferrer" class=""><code>fork(srcBucket, recovered, { snapshotVersion })</code></a>
lets me walk back. Spin up a fork pointing at Friday's snapshot, repoint
<code>TIGRIS_STORAGE_BUCKET</code>, and the next ingest builds on the recovered version.
The verdict in <code>TIGRIS_FEATURES.md</code> is "convenient, not load-bearing" and that's
accurate. Most nights it's a no-op. The night I need it, it's a one-line CLI
call.</p>
<p><strong>Presigned URLs are the delivery.</strong> The digest is the user-facing artifact of
this whole pipeline; without delivery the system doesn't do anything for me.
<a href="https://www.tigrisdata.com/docs/objects/presigned-urls/" target="_blank" rel="noopener noreferrer" class=""><code>getPresignedUrl(path, 30 * 86400)</code></a>
turns the markdown digest into a one-line URL I drop in Slack. The phone opens
it without auth, the link expires in 30 days, and the same primitive lets me
share a digest with a colleague who isn't on my GitHub or paste a wiki page into
a Notion comment without exporting anything.</p>
<p><strong><code>@tigrisdata/agent-shell</code> is what makes "scheduled and unattended"
believable.</strong> A nightly job is exactly the workload that benefits from
atomic-or-nothing writes. The buffer-then-flush pattern is built in; the
throw-discards-it semantics come along structurally. Without it you'd own the
staging prefix, the runId namespacing, the partial-flush failure mode, and the
GC sweep yourself. That's about thirty lines of pattern code that goes away once
agent-shell takes responsibility for the buffer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="i-ran-this-last-night">I ran this last night<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#i-ran-this-last-night" class="hash-link" aria-label="Direct link to I ran this last night" title="Direct link to I ran this last night" translate="no">​</a></h2>
<p>End-to-end, against a fresh Tigris bucket, with two URLs from a Hacker News RSS
feed. Here's the per-iteration log straight off stderr:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">ingest.start</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">ingest.snapshot snapshotVersion=1778000882992955252  # +2.0s</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  1 stop=tool_use tools=[fetch_url, fetch_url]            in=    512  out=  152</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  2 stop=tool_use tools=[fetch_url, list_wiki, write_source]   in= 7,425  out=1,399</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  3 stop=tool_use tools=[fetch_url, list_wiki]            in= 15,712  out=1,557</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  4 stop=tool_use tools=[write_source]                    in= 17,289  out=1,855</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  5 stop=tool_use tools=[write_wiki_page]                 in= 17,819  out=2,814</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  6 stop=tool_use tools=[write_wiki_page]                 in= 19,088  out=4,295</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  7 stop=tool_use tools=[write_wiki_page]                 in= 21,483  out=4,894</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  8 stop=tool_use tools=[write_wiki_page]                 in= 22,137  out=5,551</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter  9 stop=tool_use tools=[write_wiki_page]                 in= 22,850  out=6,130</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 10 stop=tool_use tools=[write_wiki_page]                 in= 23,482  out=6,667</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 11 stop=tool_use tools=[write_wiki_page]                 in= 24,071  out=7,570</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 12 stop=tool_use tools=[write_wiki_page]                 in= 25,562  out=8,049</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 13 stop=tool_use tools=[list_wiki]                       in= 27,025  out=8,100</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 14 stop=tool_use tools=[list_wiki, list_wiki, list_wiki] in= 27,142  out=8,228</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 15 stop=tool_use tools=[save_digest]                     in= 27,489  out=9,262</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">iter 16 stop=end_turn  tools=[]                               in= 28,798  out=9,771</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">ingest.flushed iterations=16 inputTokens=28,798 outputTokens=9,771</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">ingest.presigned digestPath=wiki/digest/2026-05-05.md</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">ingest.done newUrlsCount=2 digestUrl=https://...</span><br></div></code></pre></div></div>
<p>Three minutes fifteen seconds, sixteen iterations, eight wiki pages plus two
sources plus the digest. <strong>About $0.23 in API spend.</strong> Atomic flush at the end,
state.json updated, presigned URL generated. A separate verification I ran right
before this confirmed the discard semantics held: write a sentinel file inside
<code>withMountedBucket</code>, throw, re-mount, list the bucket. The sentinel was nowhere
to be found and the live tree was byte-for-byte identical to before the run.</p>
<p>The verification artifacts live alongside the code:
<code>scripts/experiments/shell-flush.ts</code> confirms a thrown error doesn't promote a
sentinel, <code>scripts/experiments/byte-equal-after-throw.ts</code> confirms the bucket
listing is identical pre and post. If you fork the repo and run them against
your own bucket, you get the same output. The atomicity isn't a footnote in the
docs; it's a script you can re-run any time you want to retest the claim against
a future version of agent-shell or the SDK.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-i-work-with-it">How I work with it<a href="https://www.tigrisdata.com/blog/self-updating-knowledge-base/#how-i-work-with-it" class="hash-link" aria-label="Direct link to How I work with it" title="Direct link to How I work with it" translate="no">​</a></h2>
<p>My week with the wiki has four touchpoints and that's all of them. Mornings I
read the digest in Slack on my phone over coffee, then click into one or two
pages worth reading in full. About once a week I edit <code>config/feeds.txt</code> if
there's a new vendor or paper venue I want tracked. Maybe once a month I open
Obsidian, pointed at the bucket via <code>rclone</code>, and browse the graph for
connections the agent missed. When I find a URL between scheduled runs that I
want tonight's wiki to absorb, I append it to <code>queue.txt</code> in the bucket and the
next ingest picks it up alongside the day's RSS.</p>
<p>That's the entire interaction surface. The wiki is the UI, and the wiki lives in
your Obsidian vault.</p>
<p>The FOMO is gone. Not because I'm seeing every paper (I see fewer than before)
but because I trust the wiki to surface the ones that matter, and I trust myself
to walk away from the ones it summarized for me. When you can answer "what's the
current state of binary quantization" in three seconds by clicking a
<code>[[wiki-link]]</code>, the anxiety goes somewhere quieter.</p>
<div style="max-width:36.5rem;margin-left:auto;margin-right:auto"><div><div class="is--color_gradient_back ContainerWidth_UM6U"><div class="InlineCta_fTFF sl_card_m-2 card_static cta-flex"><div class="cta-margin-left"><span class="sl_title_m fix-1px">Build your own LLM digest</span><p>The repo is open. Fork it, point it at your feeds, set five secrets, and your wiki starts updating tonight. Your first 5 GB on Tigris are free.</p></div><div class="cta-flex-item cta-margin-right"><div style="white-space:nowrap"><a href="https://github.com/davidmyriel/llm-digest" class="cta-link"><div>Get the repo<svg width="13.5" height="13.5" aria-hidden="true" viewBox="0 0 24 24" class="icon-margin iconExternalLink_node_modules-@docusaurus-theme-classNameic-lib-theme-Icon-ExternalLink-styles-module"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></svg></div></a></div></div></div></div></div></div>]]></content:encoded>
            <category>Build with Tigris</category>
            <category>AI</category>
            <category>Agents</category>
        </item>
    </channel>
</rss>