Reason Machines has released Aspen, an open-source Git server for AI agent traffic. It targets repositories that hundreds of agents push to at once. Aspen independently implements the design OpenAI described for Pando.
TL;DR
- Size: One Rust binary. Branches live in PostgreSQL (or SQLite), packs live in object storage.
- Runs on: A laptop (
aspen dev), one host via Docker Compose, or Helm and Terraform clouds. - Performance: Pushes to different branches never wait on each other, and fetches reuse stored packs.
- Best: 567 pushes per second sustained for 5 minutes on one 8-core server (self-reported).
- Worst: Landing on
mainruns at about 4 per second, and the project is pre-alpha. - Bottom line: Best: per-branch writes remove the per-repository push ceiling. Worst: no releases, no web UI, and the 24-hour durability gate is not met.
What is Aspen?
Aspen is a self-hostable Git server from Reason Machines that scales writes per branch, not per repository. It speaks plain Git over HTTPS and SSH. Packs go to S3-compatible object storage; branch history goes to PostgreSQL.
Why do classic Git servers struggle with AI agents?
An agent pushes every few minutes, and CI fetches after each push. Per Reason Machines, a classic server queues every push to a repository together. It also builds a fresh pack for every fetch. Server work grows with clients, not with new code.
OpenAI hit this wall and built Pando. It cut median push latency by nearly 5x and serves nearly a petabyte daily.
How does Aspen work?
Aspen follows Pando and drops 2 claims most Git servers keep.
Per-branch compare-and-swap
Each branch is a chain of versions in one Postgres row. A push uploads its pack, then moves the branch with one compare-and-swap. Pushes to different branches never wait for each other. If 2 pushes race on one branch, one wins. The other gets Git’s usual “fetch first” error. The cost: branches may be read at slightly different moments.
Stored packs instead of per-client packs
Aspen never unpacks a push. It checks the pack, completes missing bases and stores it as-is. A fetch streams stored packs the client lacks, recompressing nothing. Signed URLs can serve packs from a CDN.
Trunk-only advertisement and compaction
A push sees only the trunk branch, so negotiation stays small with a million branches. Background compaction merges packs 16 at a time, in levels like a log-structured merge tree. A client far behind gets a few dozen packs, not thousands. Depth-1 CI fetches get a tip pack built once and served to every job.
‘);d2.insertAdjacentHTML(‘beforeend’,”)}
R.querySelector(‘#aspC1′).textContent=”0 / “+N+’ done’;R.querySelector(‘#aspC2′).textContent=”0 / “+N+’ done’}
build();
R.querySelector(‘#aspReset’).onclick=function(){clearInterval(rt);build()};
R.querySelector(‘#aspRun’).onclick=function(){clearInterval(rt);build();var a=d1.children,b=d2.children,k=0,q=0,t=0;
for(var i=0;i
/* compaction */
var lv={1:20,2:200,3:2000,4:20000};
function plan(n){var out=[];[4096,256,16,1].forEach(function(s){while(n>=s){out.push(s);n-=s}});return out}
function comp(){var n=lv[R.querySelector(‘#aspSl’).value],ps=plan(n),s=R.querySelector(‘#aspCS’),x=10,h=””;
var tot=ps.reduce(function(a,b){return a+Math.log2(b)+1.5},0);
ps.forEach(function(p,i){var w=(Math.log2(p)+1.5)/tot*800;h+=’
h+=’
s.innerHTML=h;R.querySelector(‘#aspBeh’).textContent=n.toLocaleString()+’ versions’;
R.querySelector(‘#aspCC’).innerHTML=’Illustrative plan: ‘+ps.length+’ packs instead of ‘+n.toLocaleString()+’. Packs merge 16 at a time into 4 sizes (1, 16, 256, 4,096 here), and a fetch takes the largest pack that fits at each step.’}
R.querySelector(‘#aspSl’).oninput=comp;comp();
/* limits */
R.querySelectorAll(‘.li’).forEach(function(l){l.onclick=function(){l.classList.toggle(‘open’);setTimeout(post,400)}});
window.addEventListener(‘load’,post);window.addEventListener(‘resize’,post);setTimeout(post,300);
})();
