<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stefan Iancu</title>
    <description>The latest articles on DEV Community by Stefan Iancu (@ianqqu).</description>
    <link>https://dev.to/ianqqu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3888592%2F06103a7a-9816-45a3-a80f-941b9bfc1746.jpg</url>
      <title>DEV Community: Stefan Iancu</title>
      <link>https://dev.to/ianqqu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ianqqu"/>
    <language>en</language>
    <item>
      <title>Hit GitHub Actions limits too many times so I self hosted the CI in my living room</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Sun, 21 Jun 2026 20:06:55 +0000</pubDate>
      <link>https://dev.to/ianqqu/hit-github-actions-limits-too-many-times-so-i-self-hosted-the-ci-in-my-living-room-5gob</link>
      <guid>https://dev.to/ianqqu/hit-github-actions-limits-too-many-times-so-i-self-hosted-the-ci-in-my-living-room-5gob</guid>
      <description>&lt;p&gt;This was the second month in a row I hit the GitHub Actions minutes limit. And I'd already switched to Pro to get more - 3,000 minutes a month for private repos, at the time of writing. The bill creeps from the €4-ish I actually want to pay into double digits, and for what? Running the same builds I could run on hardware I already own.&lt;/p&gt;

&lt;p&gt;So I decided it was enough. I moved all of it onto self-hosted GitHub Actions runners: one small cloud box for deploys, and a 16GB mini PC sitting in my living room for the heavy lifting. Billed CI minutes are now zero. Here's exactly how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the minutes actually went
&lt;/h2&gt;

&lt;p&gt;First thing I did was stop guessing and look at what was eating the budget. It wasn't the deploys. Push to main, build two images, ship - that's a couple of minutes a few times a day. Annoying but not 3,000-minutes-a-month annoying.&lt;/p&gt;

&lt;p&gt;The killer was a nightly cron that runs a batch of build-and-boot tests. It's a few dozen small jobs, each one building an image and booting it to make sure nothing's quietly broken. They run on every relevant pull request, and once more every night to catch base images drifting underneath us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted runners don't burn billed minutes
&lt;/h2&gt;

&lt;p&gt;Here's the thing most people miss when they look at that bill: GitHub only charges for &lt;em&gt;GitHub-hosted&lt;/em&gt; runners. A &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;self-hosted runner&lt;/a&gt; doesn't consume your included minutes at all. The compute is yours, so GitHub stops counting.&lt;/p&gt;

&lt;p&gt;That reframes the whole problem. I don't need to make CI cheaper. I need to run it somewhere I already pay for.&lt;/p&gt;

&lt;p&gt;I briefly looked at the heavyweight option - the "run your own CI platform" tools like OVH CDS. They're genuinely good if you're an org with hundreds of pipelines and a team to babysit a distributed system. For one person trying to save €6 a month, standing up a fleet of microservices with its own database to replace 40 lines of YAML is the definition of a cure worse than the disease. I closed that tab.&lt;/p&gt;

&lt;p&gt;A GitHub Actions self-hosted runner is the opposite of that. It's a single binary that runs as a systemd service and polls GitHub for jobs over an outbound connection. Setup is about ten minutes. You don't even change your workflows beyond one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;self-hosted&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;my-label&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# was: ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Everything else - the build steps, the cache, the deploy - stays exactly as it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asterisk: GitHub already tried to charge for this once
&lt;/h2&gt;

&lt;p&gt;I should be straight about the wobble here, because if you follow this stuff you already know about it. In December 2025 GitHub announced that starting March 2026 it would charge $0.002 per minute for self-hosted runner usage on private repos - a "cloud platform charge" for the orchestration layer, even though the compute is yours. The logic was that hosted-runner revenue had been subsidising free self-hosted usage, and they wanted that subsidy back.&lt;/p&gt;

&lt;p&gt;The developer community reacted about how you'd expect to being told to pay per minute for software running on hardware they already own. The backlash was loud and fast, and within roughly a day GitHub postponed it. The hosted-runner price cuts (up to 39%, also announced that week) went ahead on January 1; the self-hosted charge got shelved "to re-evaluate."&lt;/p&gt;

&lt;p&gt;Here's the part that matters for anyone betting their CI on this: postponed is not cancelled, and GitHub was careful never to say cancelled. They kept the framing of real control-plane costs and promised only to consult before trying again. Read plainly, the intent is still there, the rollout just blew up. So the honest version of "self-hosted runners are free" is &lt;em&gt;free today, with a credible chance of a small per-minute platform fee returning later in some form.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Does that change the math? Barely, at the scale this post is about. The proposed rate was $0.002/min - my ~3,150 nightly minutes would have been about €6 a month, which is exactly the bill I was trying to escape, so that would sting. But the fee counts against your included plan minutes before it bills, so a lot of small users would never have paid a cent.&lt;/p&gt;

&lt;p&gt;And here's the actual hedge: if GitHub does reintroduce a charge for running CI on my own hardware, I move the runners to self-hosted GitLab. GitLab is open-source, you can run the whole thing yourself - the server and the runners - and there's no per-minute platform fee skimmed off compute you already own, because there's no GitHub in the middle to take a cut. GitLab Runner uses the same one-binary, polls-outbound model, so it's a lateral move, not a migration into the unknown. The workflows would need porting from Actions YAML to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, which is real work, but it's a weekend, not a rebuild. The whole point of going self-hosted was to run CI on hardware I control. A platform fee doesn't undo that - it just decides which orchestrator I point the runners at.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the part you have to get right: scope these to private repos
&lt;/h2&gt;

&lt;p&gt;One hard rule before any setup, because it's the whole reason the architecture below looks the way it does. A self-hosted runner executes whatever a workflow tells it to, on your hardware, with whatever that box can reach. Point one at a repo that accepts pull requests from strangers and a fork PR can run attacker-controlled code on your machine - then it's your secrets, your cached credentials, and a foothold on whatever network the box sits on. Because the runner connects outbound to GitHub over a trusted channel, that malicious traffic looks exactly like normal CI and your network monitoring sees nothing.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. Researchers found GitHub's own runners, plus PyTorch and Tensorflow, exploitable through exactly this fork-PR-on-a-persistent-runner pattern, and GitHub's own docs say plainly that a self-hosted runner can be persistently compromised by untrusted code in a workflow.&lt;/p&gt;

&lt;p&gt;So the rules the rest of this post quietly obeys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private repos only.&lt;/strong&gt; Every repo a self-hosted runner touches is one only I can push to. No fork PRs, no strangers, no untrusted workflow code. This single rule kills most of the attack class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The build box can't reach production.&lt;/strong&gt; It talks outbound to GitHub and the registry, nothing else. It holds no key to prod (more on that split below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it unprivileged and capped.&lt;/strong&gt; Not as root, and boxed into a resource slice so a runaway or hostile job can't take the whole machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you take one thing from this post, take the first bullet. Everything else is optimisation; that's the line between "free CI" and "free foothold into your network."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: a tiny cloud box for deploys
&lt;/h2&gt;

&lt;p&gt;I started with deploys, because those are the part that touches production and I wanted them boring and reliable.&lt;/p&gt;

&lt;p&gt;I had a small cloud VPS spare in Falkenstein, Germany - the €4-a-month kind, 4GB of RAM, same datacenter as our production servers. I installed Docker and the runner, registered it, flipped the deploy workflow to &lt;code&gt;runs-on: self-hosted&lt;/code&gt;, and the first deploy went green on the new box.&lt;/p&gt;

&lt;p&gt;Then I got a bonus I didn't plan for. Because that runner sits on the same private network as production, the deploy stopped going over the public internet entirely - it now reaches the production box over a private address. Which meant I could finally do something I'd been putting off: stop exposing production's management access to the whole internet and lock it down to just my laptop and that one runner.&lt;/p&gt;

&lt;p&gt;So step one didn't just kill some billed minutes. It pulled production's management surface off the public internet as a side effect. Good trade for an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: the real horsepower was in my living room
&lt;/h2&gt;

&lt;p&gt;The cloud box handled deploys fine, but it's a 4GB machine. The nightly batch on a single small runner is a slow, serial grind - and the heavier builds (the Next.js and Nuxt of the world) will happily try to eat more than 4GB and fall over.&lt;/p&gt;

&lt;p&gt;The box with the actual horsepower was already in my apartment. A mini PC running my homelab - 16GB of RAM, four cores, on 24/7 anyway, currently busy serving Pi-hole and a photo library to nobody in particular. That's a better build machine than the cloud VPS by a wide margin, and it costs me nothing extra because it's already plugged in.&lt;/p&gt;

&lt;p&gt;So I split the work by what it actually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build and test jobs&lt;/strong&gt; run on the living-room box. They only ever talk outbound - pull code from GitHub, push images to GitHub's container registry. They never touch production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy jobs&lt;/strong&gt; stay on the cloud box, on the private network, allowed to reach production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handoff between them is the registry. The homelab builds the image and pushes it; the cloud runner tells production to pull it. The two machines never talk to each other directly, so the homelab never needs a route into prod. That separation is the security rule from earlier made concrete: the build box is the softer target (it runs third-party action code), and I don't want a softer target holding a key to production.&lt;/p&gt;

&lt;pre&gt;                       BUILD HERE, DEPLOY THERE, PROD STAYS SEALED

  ┌──────────┐      ┌────────────────────────┐         ┌────────────────────────┐
  │ git push │      │ BUILD + CI             │         │ DEPLOY                 │
  │  → main  │ ───▶ │ sidekick · living room │         │ cloud box · Falkenstein│
  └──────────┘      │ 16 GB · 4 capped agents│         │ 4 GB · on prod network │
                    │                        │         │                        │
                    │ build images·run tests │         │ pull image · roll prod │
                    │ nightly test batch     │  image  │ reaches prod over the  │
                    │                        │ ──────▶ │ private net only       │
                    │ outbound only,         │  (GHCR) │                        │
                    │ no inbound port        │         │ prod mgmt closed to    │
                    └────────────────────────┘         │ the world              │
                                                       └───────────┬────────────┘
                                                                   │ private net
                                                                   ▼
                                                          ┌──────────────────┐
                                                          │   production     │
                                                          │   Falkenstein    │
                                                          └──────────────────┘

  Billed GitHub Actions minutes: 0
  Both boxes were already paid for, and the build box holds no route into production.
&lt;/pre&gt;
Heavy build and test work runs on the living-room box. Deploys stay on the cloud box that's allowed into production. The registry is the only handoff between them.



&lt;p&gt;One detail that makes this painless: the runner connects &lt;em&gt;outbound&lt;/em&gt;. There's no inbound port to forward, no firewall hole in my home router, and it works fine behind the carrier-grade NAT a lot of home connections sit behind. The runner dials GitHub; GitHub never dials the runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping CI from taking down the house DNS
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody warns you about when you put a CI runner on a box you actually live with. That mini PC isn't a dedicated build server. It runs Pi-hole - which is the DNS for my entire apartment. If a build pegs all four cores and starves Pi-hole, every device in the house loses name resolution. That's not a CI incident, that's a "why is the TV not working" incident.&lt;/p&gt;

&lt;p&gt;So the runners are capped. All of them live in a systemd slice with a CPU quota and a memory ceiling, leaving a core and a few gigs of headroom for the homelab stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ci-runners.slice
&lt;/span&gt;&lt;span class="nn"&gt;[Slice]&lt;/span&gt;
&lt;span class="py"&gt;CPUQuota&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;300%      # 3 of 4 cores, max, for all CI combined&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;12G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That cap does double duty: it keeps the house DNS alive, and it's also the "run it capped" rule from the security section. A job that goes hostile or just runs away can't take the whole machine with it.&lt;/p&gt;

&lt;p&gt;I also ran more than one runner agent on the box. A single agent does one job at a time, which turns that nightly batch into a long single-file queue. Four agents on a four-core box fan it out and bring a two-hour serial slog down to about half an hour. They share the same label, so GitHub load-balances jobs across them with zero workflow changes. The nightly run kicks off at 4am UTC, which is when I'm asleep and the house DNS has nothing better to do anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs now
&lt;/h2&gt;

&lt;p&gt;Zero billed GitHub Actions minutes. The cloud runner is a box I was already paying ~€4/month for, doing deploys it was idle for anyway. The homelab is hardware that was already on. I dropped from a double-digit GitHub bill back to the €4 I was always happy to pay.&lt;/p&gt;

&lt;p&gt;If you're hitting the same wall - overage emails, a Pro plan that still isn't enough, a CI bill that has nothing to do with how much you ship - the move is smaller than you think. One spare box, a binary, one line of YAML, and the discipline to only ever point it at repos you control. You probably already own the hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do self-hosted GitHub Actions runners cost money?&lt;/strong&gt;&lt;br&gt;
Not in GitHub minutes today. GitHub only bills for its hosted runners - self-hosted runners don't touch your included minutes, you just pay for the machine. The caveat: in December 2025 GitHub announced a $0.002/min platform charge for self-hosted usage on private repos, then postponed it after backlash. It's not in effect, but it's postponed rather than cancelled, so treat "free" as "free for now." See GitHub's &lt;a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions" rel="noopener noreferrer"&gt;billing docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run a self-hosted runner on a home network behind NAT?&lt;/strong&gt;&lt;br&gt;
Yes. The runner makes an outbound connection to GitHub and polls for work. There's no inbound port to open, no router config, and it works behind carrier-grade NAT. That's what makes a living-room box viable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are self-hosted runners safe?&lt;/strong&gt;&lt;br&gt;
Only if you scope them right, and getting this wrong is genuinely dangerous. The thing to never do is attach a self-hosted runner to a repo that accepts pull requests from strangers - a fork PR can run attacker-controlled code on your hardware, steal secrets, and pivot into whatever network the box can reach. Researchers found GitHub itself, PyTorch, and Tensorflow exploitable this exact way. Keep the runner on private repos you control, run it as an unprivileged user, cap it with a resource slice, and never let the box that runs untrusted action code hold credentials to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I run everything on the self-hosted runner, or keep some jobs hosted?&lt;/strong&gt;&lt;br&gt;
Split by what the job needs. Heavy, frequent builds are the ones worth moving - that's where the minutes go. A job that wants massive parallelism (a big test matrix finishing in five minutes across dozens of ephemeral machines) can actually be a poor fit for one or two self-hosted agents. Move the minute-burners, leave the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a self-hosted runner and a full CI platform like Jenkins or CDS?&lt;/strong&gt;&lt;br&gt;
Scale and babysitting. A self-hosted GitHub Actions runner is a single agent that plugs into the GitHub Actions you already use - your existing workflows run unchanged. A full self-hosted CI platform replaces GitHub Actions entirely and is a distributed system you operate yourself. For one person or a small team, the runner is almost always the right call.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>infrastructure</category>
      <category>programming</category>
      <category>automation</category>
    </item>
    <item>
      <title>An EU E2B alternative: agent sandboxes that stay in the EU</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Tue, 16 Jun 2026 11:55:43 +0000</pubDate>
      <link>https://dev.to/ianqqu/an-eu-e2b-alternative-agent-sandboxes-that-stay-in-the-eu-1k1b</link>
      <guid>https://dev.to/ianqqu/an-eu-e2b-alternative-agent-sandboxes-that-stay-in-the-eu-1k1b</guid>
      <description>&lt;p&gt;If you're shopping for an E2B alternative, you've probably already decided that running agent-generated code on your own boxes is a bad idea. Good. The next question is the one that sends procurement into a three-week loop: &lt;em&gt;where does that code actually run, and who can be compelled to hand over what it touched?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://e2b.dev" rel="noopener noreferrer"&gt;E2B&lt;/a&gt; is a genuinely good product - fast microVM isolation and mature SDKs. We use the same primitives. But it's a US company, and for a lot of EU teams that single fact decides it. This is orkestr vs E2B: where we line up, where we differ, and the one piece of the stack that's never going to be in the EU no matter who you pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short verdict
&lt;/h2&gt;

&lt;p&gt;Pick E2B if you're US-hosted, want US-region latency, and the bigger existing ecosystem. Pick orkestr if you're an EU company that wants the runtime, the snapshots, and the DPA all sitting inside one EU legal entity - on every plan, not just enterprise.&lt;/p&gt;

&lt;p&gt;That's it. The rest is detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually the same
&lt;/h2&gt;

&lt;p&gt;The products are close. The shape is nearly identical, because this is a problem with a known-good shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A REST API plus Python and JavaScript SDKs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create&lt;/code&gt; / &lt;code&gt;exec&lt;/code&gt; / read+write files / &lt;code&gt;pause&lt;/code&gt; / &lt;code&gt;resume&lt;/code&gt; / &lt;code&gt;terminate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An MCP server you can drop into Claude Code, Cursor, or any MCP client.&lt;/li&gt;
&lt;li&gt;Hardware-isolated VMs, not shared-kernel containers. Each sandbox boots its own kernel and rootfs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the orkestr Python SDK. If you've written E2B code, this will look familiar on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orkestr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(sum(range(1_000_000)))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python /workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 499999500000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if "same primitives, different jurisdiction" is all you needed to hear, you can &lt;a href="https://orkestr.eu/sandboxes" rel="noopener noreferrer"&gt;start free&lt;/a&gt; - no waitlist, no card. If you want the actual differences, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that is &lt;em&gt;not&lt;/em&gt; in the EU (and never will be)
&lt;/h2&gt;

&lt;p&gt;An AI agent has two halves: the &lt;strong&gt;model&lt;/strong&gt; that decides what to do, and the &lt;strong&gt;sandbox&lt;/strong&gt; that runs what it decided. Those are different machines, often different companies.&lt;/p&gt;

&lt;pre&gt;                       ONE AGENT LOOP, TWO DIFFERENT MACHINES

  THE MODEL CALL                                EU · GERMANY &amp;amp; FINLAND
  you pick the provider                         operated by an EU entity, no US parent
  ┌────────────────────────────┐                ┌──────────────────────────────────┐
  │  Anthropic / OpenAI   (US) │  run this code │       orkestr sandbox            │
  │  Mistral              (EU) │  ────────────▶ │  · code execution                │
  │  self-hosted        (yours)│                │  · files in /workspace           │
  │                            │  ◀──────────── │  · pause / resume snapshots      │
  │  crosses the border only   │  stdout·files  │  · env vars · logs               │
  │  if you pick a US model.   │                │                                  │
  │  your DPA, with them.      │                │  none of this leaves the EU      │
  └────────────────────────────┘                └──────────────────────────────────┘

  orkestr runs the SANDBOX, not the model. the code, files and snapshots stay in
  the EU - the reasoning step is the only part you can send elsewhere.
&lt;/pre&gt;
An agent is two machines: a model that decides and a sandbox that runs. orkestr operates the second one, in the EU.



&lt;p&gt;orkestr runs the &lt;strong&gt;sandbox&lt;/strong&gt;. We do not run the model. When your agent's brain decides to &lt;code&gt;pip install pandas and parse this CSV&lt;/code&gt;, that &lt;em&gt;decision&lt;/em&gt; came from whatever model you pointed it at - and if that's a US model API, the prompt and completion are processed under that provider's terms, not ours. That's your data processor to choose and your DPA to sign.&lt;/p&gt;

&lt;p&gt;What orkestr guarantees is everything on the right-hand side: the code that runs, the files it touches, the memory it allocates, the snapshots we take when you pause it. All of that lives on bare-metal hardware in Germany and Finland and never leaves the EU. We can't promise anything about a model we don't host, so we don't. We promise the part we run.&lt;/p&gt;

&lt;p&gt;For a lot of teams this is the ideal arrangement - pick a European model like &lt;a href="https://mistral.ai" rel="noopener noreferrer"&gt;Mistral&lt;/a&gt; and the &lt;em&gt;entire&lt;/em&gt; loop is in the EU; pick a US model and only the reasoning step crosses the border while all the working data stays home. Either way, the blast radius of "agent ran some code" is EU-contained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jurisdiction, not just region
&lt;/h2&gt;

&lt;p&gt;The real difference isn't where the servers sit. It's who controls the company that runs them.&lt;/p&gt;

&lt;p&gt;E2B can run in Europe, but look at how you get there. The managed E2B service is operated by FoundryLabs, Inc., a Delaware corporation. To keep your sandbox data in the EU, the documented path is bring-your-own-cloud - you run their stack in your own AWS, GCP, or Azure account or VPC - or you self-host it outright. That's a real option, and for a large team that already lives in one of those clouds, a good one. It's also a project: you're now operating sandbox infrastructure, patching it, and watching it. The click-and-go managed version still runs under a US entity.&lt;/p&gt;

&lt;p&gt;And jurisdiction follows the entity, not just the bytes. Under the US CLOUD Act, a US-incorporated provider can be compelled to produce data it controls regardless of where that data physically sits. Post-Schrems II, that control question is exactly what EU data protection authorities care about. A German DPA isn't only asking "where are the bytes" - it's asking "whose lawyer answers the subpoena."&lt;/p&gt;

&lt;p&gt;With orkestr, the company that signs your DPA is the company that operates the runtime, and it's an EU entity with no US parent three layers up. EU hosting isn't a tier, and it isn't a bring-your-own-cloud project - it's where everything runs by default, including on the free plan. You don't negotiate or self-host your way to Europe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;E2B&lt;/th&gt;
&lt;th&gt;orkestr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Company jurisdiction&lt;/td&gt;
&lt;td&gt;US (Delaware)&lt;/td&gt;
&lt;td&gt;EU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EU data residency&lt;/td&gt;
&lt;td&gt;Via BYOC / self-host&lt;/td&gt;
&lt;td&gt;Default, every plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DPA signed by&lt;/td&gt;
&lt;td&gt;US entity&lt;/td&gt;
&lt;td&gt;EU operating entity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subject to US CLOUD Act&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;microVM, hardware-isolated&lt;/td&gt;
&lt;td&gt;microVM, hardware-isolated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDKs&lt;/td&gt;
&lt;td&gt;Python, JS&lt;/td&gt;
&lt;td&gt;Python, JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause / resume&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress default&lt;/td&gt;
&lt;td&gt;Internet on (opt out)&lt;/td&gt;
&lt;td&gt;Off (opt in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;Hobby free + usage; Pro $150/mo + usage&lt;/td&gt;
&lt;td&gt;Free to start; per-second usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open source / self-host&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (managed only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Isolation is a tie, and that's the point: both run each sandbox as its own hardware-virtualised microVM on bare metal, so the boundary is the same. Decide on the rows where the columns actually differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data retention: what we keep, and for how long
&lt;/h2&gt;

&lt;p&gt;GDPR isn't only about where data sits - it's about minimisation and deletion. The interesting question for any sandbox provider is what survives after your agent is done. Here's the orkestr answer, concretely:&lt;/p&gt;

&lt;pre&gt;                          LIFECYCLE, AND WHAT SURVIVES IT

       boot              pause                resume · state restored
  create ──▶ running ──────────▶ paused  ◀──────────────────┐
                │                (snapshot on               │
                │                 EU storage) ──────────────┘
                │ terminate
                ▼
  ┌────────────────────────────────────────────┐
  │  terminated → destroyed                    │
  │  kernel, rootfs, processes, memory - gone  │
  └────────────────────────────────────────────┘

  WHAT'S LEFT AFTERWARD
  ┌─────────────────────────┬─────────────────────────┬─────────────────────────┐
  │ Environment variables   │ Snapshots               │ Terminated sandbox      │
  │ in memory only, never   │ capped per plan,        │ nothing runnable behind │
  │ written to disk. gone   │ deleted on terminate.   │ it - no filesystem, no  │
  │ with the VM.            │ for parking a task.     │ process tree.           │
  └─────────────────────────┴─────────────────────────┴─────────────────────────┘
&lt;/pre&gt;
Pause snapshots state to EU storage so an agent can park mid-task; terminate destroys the whole VM and leaves nothing runnable behind.



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; are held in memory for the life of the sandbox and never written to a persistent store. They die with the VM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots&lt;/strong&gt; (from &lt;code&gt;pause&lt;/code&gt;) are capped per plan, and they're deleted when you &lt;code&gt;terminate&lt;/code&gt; the sandbox. They exist to let an agent park mid-task and pick up an hour later - not to accumulate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A terminated sandbox leaves nothing runnable behind.&lt;/strong&gt; The kernel, the filesystem overlay, the process tree - destroyed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We keep what we need to run the sandbox, and not more.&lt;/strong&gt; If a field isn't needed to operate the runtime or bill it, we don't store it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still own the data you deliberately persist elsewhere - if your agent writes to a database, that's your store. But the sandbox itself is built to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance, briefly
&lt;/h2&gt;

&lt;p&gt;This is where people expect the EU option to apologise, and we don't have to. Cold start is around 150ms; from a warm pool it's under 30ms. That's in the same class as E2B - microVMs are microVMs, and we keep a pool hot so your agent isn't waiting on a boot.&lt;/p&gt;

&lt;p&gt;The honest latency caveat is geography, not architecture: if your users and your model API are on the US east coast, a sandbox in Frankfurt adds a transatlantic round trip versus a US-region sandbox. For most agent workloads - where the expensive step is the model call, not the sandbox hop - it disappears into the noise. If you're latency-bound on the sandbox specifically and your traffic is US-centric, that's a fair reason to stay US-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network egress is a choice, not a default
&lt;/h2&gt;

&lt;p&gt;One more thing that matters the moment a model is writing the commands. Every orkestr sandbox picks a network mode at creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# no egress at all - the safe default for code you haven't read
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# allowlist - package registries and common APIs work, invented domains don't
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;off&lt;/code&gt; means nothing leaves the box. &lt;code&gt;restricted&lt;/code&gt; routes through an allowlisting proxy so &lt;code&gt;pip install&lt;/code&gt; works but a hallucinated &lt;code&gt;curl evil.example.com&lt;/code&gt; doesn't. &lt;code&gt;open&lt;/code&gt; is full egress and it's gated behind a verified payment method on purpose. The default is the paranoid one, which is the right default for LLM-generated code.&lt;/p&gt;

&lt;p&gt;E2B can lock egress down too, and it's flexible about it: outbound is &lt;a href="https://e2b.dev/docs/sandbox/internet-access" rel="noopener noreferrer"&gt;on by default&lt;/a&gt;, and you opt into restriction with allow/deny rules and domain filtering you can even change on a running sandbox. The difference is which way the default points. E2B ships open and you close it; orkestr ships closed and you open it. When the thing writing the commands is an LLM you haven't audited, default-closed is the safer place to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to pick which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick E2B if:&lt;/strong&gt; you're already US-hosted and latency-sensitive on the sandbox itself, you want the bigger existing ecosystem, or you need their specific templates today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick orkestr if:&lt;/strong&gt; you're an EU company (or sell to one) and "the entity running our agent code is US-incorporated" is a sentence you don't want in a procurement doc - and you want that to be true on the free tier, not just enterprise.&lt;/p&gt;

&lt;p&gt;If that's you, you can &lt;a href="https://orkestr.eu/sandboxes" rel="noopener noreferrer"&gt;start free&lt;/a&gt; - no waitlist, no credit card. Enable sandboxes from the console in one click, then create your first one from the dashboard or the SDK. For the bigger picture on why we built an &lt;a href="https://dev.to/eu-sandbox-for-ai-agents/"&gt;EU sandbox for AI agents&lt;/a&gt; in the first place, or a plain-language take on &lt;a href="https://dev.to/managed-sandboxes-for-ai-agents/"&gt;what a managed sandbox actually is&lt;/a&gt;, those two posts go deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can't I just run E2B in the EU?&lt;/strong&gt;&lt;br&gt;
You can, by bringing your own cloud or self-hosting - running their stack in your own AWS, GCP, or Azure account so the data stays in your EU infrastructure. But then you operate it. The managed service is run by a US (Delaware) company, and GDPR cares about who &lt;em&gt;controls&lt;/em&gt; the data, not just where it sits: a US-incorporated provider can be compelled under the US CLOUD Act regardless of region. orkestr's operating entity is in the EU with no US parent, and EU hosting is the default on every plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does my data leave the EU at all?&lt;/strong&gt;&lt;br&gt;
The sandbox, its files, its memory, and its snapshots stay in the EU. The one exception is the model call - if your agent uses a US-hosted LLM, the prompt and completion are processed by that provider under your DPA with them. Use an EU model and the whole loop stays in Europe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use it with Claude Code or Claude's Managed Agents?&lt;/strong&gt;&lt;br&gt;
Yes. There's an MCP server for Claude Code and Cursor, and the REST API works as a configured sandbox provider for agent frameworks that let you point at one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What languages are supported?&lt;/strong&gt;&lt;br&gt;
Python 3.12 and Node 22 templates ship today, plus a bare Python image and a minimal Ubuntu shell. Custom images are on the roadmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How fast is it?&lt;/strong&gt;&lt;br&gt;
Around 150ms cold, under 30ms from a warm pool - the same class as other microVM sandboxes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>agents</category>
    </item>
    <item>
      <title>A serverless QR code API in 30 lines</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Sat, 13 Jun 2026 11:58:05 +0000</pubDate>
      <link>https://dev.to/ianqqu/a-serverless-qr-code-api-in-30-lines-31gg</link>
      <guid>https://dev.to/ianqqu/a-serverless-qr-code-api-in-30-lines-31gg</guid>
      <description>&lt;p&gt;Most of what gets written about serverless is about scale. Cold starts at p99, concurrency limits, fan-out, how some company served a billion requests without a server in sight. Fine. But that's not why I love these things.&lt;/p&gt;

&lt;p&gt;I love them for the opposite reason. The function that does one small thing. Thirty lines, a URL, done. No repo, no Dockerfile, no CI pipeline, no &lt;code&gt;terraform plan&lt;/code&gt; you read three times before running. You have an idea on a Tuesday and by lunch it's a live endpoint you can curl. That's the part nobody puts on a conference slide.&lt;/p&gt;

&lt;p&gt;So here's a little homage to that. We're going to build a serverless QR code API. It's genuinely about 30 lines, it's live in under a minute, and by the end you'll have a URL that turns any text into a scannable QR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing
&lt;/h2&gt;

&lt;p&gt;A function on &lt;a href="https://orkestr.eu/functions" rel="noopener noreferrer"&gt;orkestr Functions&lt;/a&gt; is a single handler. The runtime hands you an &lt;code&gt;event&lt;/code&gt; (method, path, query, headers, body), you return a dict. If that dict has a &lt;code&gt;statusCode&lt;/code&gt;, it's treated as a full HTTP response, and the &lt;code&gt;body&lt;/code&gt; can be raw bytes. Which means a function can return a PNG and the browser just renders it.&lt;/p&gt;

&lt;p&gt;That last detail is the whole trick. Here's the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;segno&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://orkestr.eu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Bare domains like "orkestr.eu" decode as plain text on phones and do
&lt;/span&gt;    &lt;span class="c1"&gt;# nothing when scanned. Promote anything without a scheme to https so the
&lt;/span&gt;    &lt;span class="c1"&gt;# QR is always tappable.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mailto:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tel:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wifi:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;

    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;segno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one dependency is &lt;a href="https://segno.readthedocs.io/" rel="noopener noreferrer"&gt;segno&lt;/a&gt;, a pure-Python QR library. No Pillow, no system libraries, so the build stays around five seconds instead of dragging in a C toolchain. You add it by typing &lt;code&gt;segno&lt;/code&gt; into the Dependencies box. That's it. (Dependencies are a &lt;a href="https://blog.orkestr.eu/functions-now-with-dependencies/" rel="noopener noreferrer"&gt;recent-ish addition&lt;/a&gt;; the first beta shipped with stdlib only.)&lt;/p&gt;

&lt;p&gt;Paste the handler, pick Python 3.12, deploy. About a minute later you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://fn-qr.orkestr.run/?data=https://orkestr.eu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that and a QR code fills the tab. Point your phone at it and it offers to open orkestr.eu. There's no step five.&lt;/p&gt;

&lt;p&gt;That URL is public, which is what you want for a QR generator. It doesn't have to be. Every function has two auth modes: public, or API-key. Flip it to API-key and orkestr issues a key for you (they're shaped &lt;code&gt;ork_fn_...&lt;/code&gt;). Callers pass it in an &lt;code&gt;X-Orkestr-API-Key&lt;/code&gt; header, and anything without the right key gets a 401. You can rotate the key whenever you want. For the QR toy, public is fine. For a webhook receiver sitting on a signing secret, you'd gate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is good for (and what it isn't)
&lt;/h2&gt;

&lt;p&gt;A QR endpoint is a toy, but the shape is not. The shape is: small input, small output, one job, public URL. That shape is everywhere once you start looking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A webhook receiver that validates a Mollie payment webhook and forwards the payload.&lt;/li&gt;
&lt;li&gt;An OG-image generator that takes &lt;code&gt;?title=&lt;/code&gt; and returns a PNG.&lt;/li&gt;
&lt;li&gt;A short-link redirector backed by an env var or a single API call.&lt;/li&gt;
&lt;li&gt;A "contact form" endpoint for an otherwise static site.&lt;/li&gt;
&lt;li&gt;A badge generator (this whole blog's status badges are basically this function with different bytes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these want a Next.js app around them. They want a function. Reaching for a full project with a Dockerfile and a deploy pipeline to host 30 lines is how you end up paying €70 a month and maintaining a YAML file for something that should be free and forgotten.&lt;/p&gt;

&lt;p&gt;What it isn't good for: anything stateful, anything that wants a real database connection pool, anything compute-heavy, or a full web framework with dozens of routes. Those are apps. Deploy them as apps. The function is for the one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;On servers in Falkenstein, Germany. Same hardware as the rest of orkestr, same container hardening (cap-drop, no-new-privileges, non-root). Your handler is encrypted at rest. No US data transfer, which for a QR generator is irrelevant and for the Mollie-webhook version is the entire ballgame.&lt;/p&gt;

&lt;p&gt;The container scales to zero after about a minute of no traffic, so an idle function costs nothing and holds no capacity. The first request after it sleeps pays a cold start - roughly 3 seconds on the free tier, faster on paid where you get more vCPU. I wrote about that &lt;a href="https://blog.orkestr.eu/functions-beta/" rel="noopener noreferrer"&gt;tradeoff honestly in the beta post&lt;/a&gt; if you want the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it yourself
&lt;/h2&gt;

&lt;p&gt;One function is free on the &lt;a href="https://orkestr.eu/pricing" rel="noopener noreferrer"&gt;Starter plan&lt;/a&gt; - not a trial, just free. Paste the handler above, add &lt;code&gt;segno&lt;/code&gt;, deploy. You'll have a working QR code API on your own subdomain before your coffee's cold.&lt;/p&gt;

&lt;p&gt;And then, ideally, you forget about it. That's the highest compliment you can pay a tiny function. It just sits there at its URL, turning text into QR codes, asking nothing of you. The best infrastructure is the kind you stop thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the code
&lt;/h2&gt;

&lt;p&gt;The full thing - handler, the one-line &lt;code&gt;requirements.txt&lt;/code&gt;, and a README - lives at &lt;a href="https://codeberg.org/orkestr/qr-code" rel="noopener noreferrer"&gt;codeberg.org/orkestr/qr-code&lt;/a&gt;. Copy &lt;code&gt;handler.py&lt;/code&gt; into a new function, paste &lt;code&gt;segno&lt;/code&gt; into the Dependencies box, deploy. Nothing else to wire up.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>A self-hosted Google reCAPTCHA alternative (we ship it)</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Fri, 29 May 2026 06:29:01 +0000</pubDate>
      <link>https://dev.to/ianqqu/a-self-hosted-google-recaptcha-alternative-we-ship-it-276h</link>
      <guid>https://dev.to/ianqqu/a-self-hosted-google-recaptcha-alternative-we-ship-it-276h</guid>
      <description>&lt;p&gt;You know the one. You're trying to subscribe to a newsletter, and Google asks you to identify all the bicycles. You squint at the photo. You miss one pixel. You get a new grid, this time with motorcycles, and you start wondering if you're the bot.&lt;/p&gt;

&lt;p&gt;That's reCAPTCHA. It works, kind of. It also loads a JavaScript bundle from &lt;code&gt;google.com&lt;/code&gt; on every page it appears on, ships your browsing behavior to Google's risk-scoring API, and gets flagged in nearly every cookie-compliance audit we've seen.&lt;/p&gt;

&lt;p&gt;The captcha on &lt;a href="https://orkestr.eu" rel="noopener noreferrer"&gt;orkestr&lt;/a&gt; is &lt;a href="https://altcha.org/" rel="noopener noreferrer"&gt;Altcha&lt;/a&gt;. Self-hosted, no third-party JS, no Google, no behavioral tracking. Users don't see a puzzle. There's a single checkbox and a quiet "verifying..." while the browser does some math. About a second, total.&lt;/p&gt;

&lt;p&gt;This post is why we picked it over the better-known options. If you're looking for a Google reCAPTCHA alternative and you care about GDPR, this is the path we'd recommend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest verdict, up front
&lt;/h2&gt;

&lt;p&gt;For low-to-moderate traffic forms (signup, contact, comments, login) where you're not under sustained ML-driven attack: &lt;strong&gt;self-hosted Altcha is the right pick&lt;/strong&gt;. No third-party calls, no GDPR paperwork, no user-hostile puzzles, a small widget loaded from your own origin.&lt;/p&gt;

&lt;p&gt;For very high-traffic public surfaces under active attack by sophisticated operators (e-commerce checkouts, ticket drops, large social platforms) you probably still want a managed service with behavioral ML. Altcha's proof-of-work raises the per-request cost; at internet scale, a real bot farm absorbs it.&lt;/p&gt;

&lt;p&gt;"Low-to-moderate traffic forms" is most of the internet. Altcha wins for most people.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they compare
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Google reCAPTCHA v3&lt;/th&gt;
&lt;th&gt;hCaptcha&lt;/th&gt;
&lt;th&gt;Cloudflare Turnstile&lt;/th&gt;
&lt;th&gt;Altcha (self-hosted)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data leaves your origin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (google.com)&lt;/td&gt;
&lt;td&gt;Yes (hcaptcha.com)&lt;/td&gt;
&lt;td&gt;Yes (cloudflare.com)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavior tracking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JS bundle (gzipped)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;300+ KB&lt;/td&gt;
&lt;td&gt;250+ KB&lt;/td&gt;
&lt;td&gt;85+ KB&lt;/td&gt;
&lt;td&gt;34 KB, your origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User puzzle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often&lt;/td&gt;
&lt;td&gt;Often&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GDPR posture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;US sub-processor&lt;/td&gt;
&lt;td&gt;US sub-processor&lt;/td&gt;
&lt;td&gt;US sub-processor&lt;/td&gt;
&lt;td&gt;None, it's your server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hostable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free + paid tiers&lt;/td&gt;
&lt;td&gt;Free + paid tiers&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free (your CPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "data leaves your origin" row is the whole point. Every other column flows from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What reCAPTCHA actually does
&lt;/h2&gt;

&lt;p&gt;reCAPTCHA v3 (the "invisible" one) is not really a captcha. It's a behavioral fingerprinting library that returns a score between 0 (likely bot) and 1 (likely human). The score comes from Google looking at your browser, your IP, your mouse movement, your cookies, and crucially, your entire Google account state if you happen to be logged in.&lt;/p&gt;

&lt;p&gt;That's a feature for fraud detection at scale. It's a legal and reputational risk for an EU business, because every form on your site becomes a Google data-collection endpoint. Schrems II turned every cross-border transfer to a US ad-tech company into a per-tool compliance question. reCAPTCHA is exactly that tool.&lt;/p&gt;

&lt;p&gt;The compliance picture got materially worse on April 2, 2026. Google reclassified reCAPTCHA from data controller to data processor, which sounds like an internal Google detail but isn't. EU operators are now fully on the hook for GDPR compliance on every reCAPTCHA hit: a signed DPA with Google, an explicit legal basis documented in your privacy policy, and your team handling any data subject requests. Data still goes to US servers. The compliance burden didn't shrink, it moved to your desk.&lt;/p&gt;

&lt;p&gt;EU DPOs ask the same question in different words: if you don't control the JavaScript on your login page, who does? It's worth sitting with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Turnstile is better, but it's still Cloudflare
&lt;/h2&gt;

&lt;p&gt;Turnstile is the industry response to reCAPTCHA-fatigue. It's free, the UX is good (most users see a checkbox, not a puzzle), and Cloudflare is more transparent about what it collects than Google has ever been. If you're not in the EU and you don't care about CLOUD Act exposure, Turnstile is a fine pick. It's the second-best option here.&lt;/p&gt;

&lt;p&gt;For an EU-native deploy platform, though, every US sub-processor is a row on a DPA that EU buyers read line by line. We wanted captcha to be a thing on our origin, not a thing on someone else's. Turnstile doesn't get you there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Altcha does instead
&lt;/h2&gt;

&lt;p&gt;Altcha replaces the "score-the-user" model with a "make-the-bot-pay" model. Proof of work.&lt;/p&gt;

&lt;p&gt;The server generates a random salt, a nonce, and a target prefix, signs the whole envelope with an HMAC key only the server knows, and hands it to the browser. A Web Worker iterates a counter, combining it with the nonce and running PBKDF2-SHA256 each step, until the derived key starts with the target prefix. When it finds a matching counter, the server gets the challenge plus the solution back, verifies the HMAC, verifies the prefix match, and accepts the form.&lt;/p&gt;

&lt;p&gt;The whole thing takes about a second on a modern laptop. A human doesn't notice. A bot farm trying to submit ten thousand forms per second per IP suddenly has to spin up real CPU per submission.&lt;/p&gt;

&lt;p&gt;There's no third party. The challenge endpoint sits on your domain. The widget JavaScript serves from your origin. The HMAC key never leaves your server. There's nothing to add to a sub-processor list, because there's no sub-processor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two gotchas if you self-host Altcha
&lt;/h2&gt;

&lt;p&gt;The integration is small. One new endpoint, one HMAC environment variable (&lt;code&gt;openssl rand -hex 32&lt;/code&gt;), one React component for the widget. Two things are easy to miss, both worth flagging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't let a CDN cache the challenge
&lt;/h3&gt;

&lt;p&gt;We sit behind &lt;a href="https://bunny.net" rel="noopener noreferrer"&gt;Bunny CDN&lt;/a&gt; for SSL and edge caching. Our first wiring of Altcha had a weird failure mode: legitimate users were intermittently getting "captcha already used" errors on first submit.&lt;/p&gt;

&lt;p&gt;The cause was the obvious one in hindsight. The CDN was caching responses from &lt;code&gt;GET /api/altcha/challenge&lt;/code&gt;. Two different users were getting the same signed challenge, the first submission consumed it via replay protection, the second user hit a 400.&lt;/p&gt;

&lt;p&gt;The fix is one header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/altcha/challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_challenge&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mint_challenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hmac_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cache-Control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-store&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're behind any CDN, set this or you'll spend an afternoon wondering why your captcha is mass-failing in production but works locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replay protection needs a real store
&lt;/h3&gt;

&lt;p&gt;The replay attack on a captcha is obvious: capture a valid submission, replay it many times. Most official Altcha libraries (Django, .NET, and others) ship with a replay store built in, so check yours before reinventing it. We rolled our own integration against FastAPI, which meant we added one ourselves. Redis &lt;code&gt;SETNX&lt;/code&gt; keyed on the verified solution hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Returns True if key was set (first use), False if it already existed (replay)
&lt;/span&gt;&lt;span class="n"&gt;is_first_use&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;altcha:used:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;solution_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 10 minutes, safely longer than the challenge TTL
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_first_use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captcha already used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An in-memory dict won't survive a deploy or scale across worker processes. Redis works. Postgres works. Pick something durable, pick a TTL longer than your challenge expiry, and you're done.&lt;/p&gt;

&lt;p&gt;We use a separate HMAC key per environment, so a leak in dev can't be used to forge prod challenges. Same reasoning as keeping your test Stripe keys separate from your live keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Altcha is the wrong call
&lt;/h2&gt;

&lt;p&gt;We want to be honest about the limits, because they're real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're being actively attacked by a sophisticated bot operator.&lt;/strong&gt; A real bot farm with cloud capacity can absorb proof-of-work costs at the bot's marginal cost of CPU. PoW raises the floor; it doesn't put a ceiling on a determined attacker. If you've ever had to pull pricing data off your site after a competitor scrape, you want behavioral ML, not a math puzzle. Altcha also supports Argon2id and Scrypt as memory-hard alternatives to PBKDF2, which resist GPU and ASIC acceleration and narrow the gap between consumer devices and bot farms; not a silver bullet, but worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your users are on very low-end devices.&lt;/strong&gt; A one-second solve on a modern laptop can become a four-second solve on a five-year-old budget phone. If your audience is consumer mobile in developing markets, benchmark before shipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't run a Redis (or equivalent) for replay protection.&lt;/strong&gt; A captcha without single-use enforcement is theater. If your hosting tier doesn't let you add a key-value store, fix that first, then come back.&lt;/p&gt;

&lt;p&gt;For everything else (the long tail of SaaS signup forms, internal admin tools, contact forms, comment sections, account recovery flows), Altcha is the better default than reCAPTCHA in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Altcha actually free?&lt;/strong&gt;&lt;br&gt;
Yes. It's MIT-licensed open source. The library is at &lt;a href="https://github.com/altcha-org/altcha" rel="noopener noreferrer"&gt;github.com/altcha-org/altcha&lt;/a&gt;. The cost is the CPU spent on the verifying server (negligible) and the client-side compute (about a second of one core, once per submission).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to run a separate Altcha server?&lt;/strong&gt;&lt;br&gt;
No. Altcha is a protocol plus a small client library. You add two endpoints to your existing backend: one to mint challenges, one to verify them. There's no Altcha-the-service unless you opt into their managed offering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Altcha work without JavaScript?&lt;/strong&gt;&lt;br&gt;
The default flow needs JavaScript (the Web Worker is where the proof-of-work runs). Altcha also supports a server-side mode where a pre-computed challenge can be verified at form-submit time, useful for accessibility tooling. Most setups won't need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will switching from reCAPTCHA to Altcha break my form analytics?&lt;/strong&gt;&lt;br&gt;
Only if you were using reCAPTCHA's "human score" as a continuous signal in your analytics. If it was a binary pass/fail gating form submission, the switch is transparent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Altcha on a static site?&lt;/strong&gt;&lt;br&gt;
You need somewhere to mint and verify challenges. A serverless function counts. A small serverless function handles this in about 30 lines of Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Captcha is one of those problems that looks solved until you read the privacy policy. Most teams pick reCAPTCHA because it's the default, not because they evaluated it. If you're an EU business, you almost certainly didn't evaluate it. Your DPO will, eventually, and the conversation is going to be uncomfortable.&lt;/p&gt;

&lt;p&gt;We chose Altcha because it was the only option that kept the entire flow on our origin. You can run the same thing on whatever backend you already have. It's a couple of hundred lines of code and one Redis key prefix.&lt;/p&gt;

&lt;p&gt;If you also care about which other US services your stack quietly depends on, the &lt;a href="https://blog.orkestr.eu/vercel-april-2026-breach-checklist/" rel="noopener noreferrer"&gt;Vercel April 2026 breach checklist&lt;/a&gt; is a useful companion read.&lt;/p&gt;

</description>
      <category>development</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What are managed sandboxes for AI agents?</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Tue, 26 May 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/ianqqu/what-are-managed-sandboxes-for-ai-agents-45ad</link>
      <guid>https://dev.to/ianqqu/what-are-managed-sandboxes-for-ai-agents-45ad</guid>
      <description>&lt;p&gt;Ask an AI agent to "figure out why this script is slow" and it will. It writes code, installs three packages you've never heard of, runs them, maybe shells out to &lt;code&gt;git&lt;/code&gt; to check something. The question is &lt;em&gt;where&lt;/em&gt; all that runs. If the answer is "your laptop," you've handed a language model write access to your filesystem.&lt;/p&gt;

&lt;p&gt;A managed sandbox for AI agents is the fix. It's a throwaway computer your agent gets, does whatever it wants inside, and hands back. "Managed" means you don't run it - one API call and it exists, one call and it's gone. That's the whole idea. The rest of this post is what that actually means, with real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sandbox is just a throwaway computer
&lt;/h2&gt;

&lt;p&gt;Strip away the jargon and a sandbox is a small, isolated computer that exists for a few seconds or a few minutes and then disappears. Your agent gets to be reckless inside it. It can &lt;code&gt;rm -rf&lt;/code&gt; the wrong directory, install a broken package, run an infinite loop - and the worst case is "that sandbox crashed," not "my git history is gone."&lt;/p&gt;

&lt;p&gt;Here's the entire lifecycle with the orkestr Python SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orkestr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(sum(range(1_000_000)))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python /workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# 499999500000
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# ~120
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create it, write a file, run a command, read the output. The &lt;code&gt;with&lt;/code&gt; block terminates the sandbox when you leave it - even if your code throws halfway through. No leftover process, no lingering bill.&lt;/p&gt;

&lt;p&gt;That's it. If your agent can call a function, it can use a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "managed" part means you run nothing
&lt;/h2&gt;

&lt;p&gt;Plenty of teams build their own version of this. They spin up a VM, install Docker, write a queue, handle cleanup, patch the host, watch it 24/7. That's a real project, and it's not the project you actually wanted to ship.&lt;/p&gt;

&lt;p&gt;"Managed" means that whole layer is gone. You call &lt;code&gt;POST /v1/sandboxes&lt;/code&gt;, you get back a sandbox ID, you use it. No host to patch, no capacity to plan, no cleanup cron. The sandbox boots from a &lt;a href="https://dev.to/eu-sandbox-for-ai-agents/"&gt;warm pool&lt;/a&gt; in under 30ms, and a cold boot is around 150ms - fast enough that your agent doesn't wait on it.&lt;/p&gt;

&lt;p&gt;There are four templates to start from:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Template&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-3.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPython 3.12 with pip and common libs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-3.12-bare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPython 3.12 only, faster start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-22&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node 22 with npm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ubuntu-24.04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A minimal Ubuntu shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick one at create time. The sandbox comes up with that runtime already installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  It can't reach anything it shouldn't
&lt;/h2&gt;

&lt;p&gt;Here's the part that matters once an LLM is generating the commands. A sandbox isn't useful if it can quietly POST your data to an address the model invented. So network access is a setting you choose, not a default you inherit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# no internet at all - the safe default for code you haven't read
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# allowlist - pip and npm work, random domains don't
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three modes. &lt;code&gt;off&lt;/code&gt; means no egress - nothing leaves the box. &lt;code&gt;restricted&lt;/code&gt; routes traffic through an allowlisting proxy: the sandbox can reach package registries, GitHub, and the major LLM APIs, and that's the list. Your agent can &lt;code&gt;pip install pandas&lt;/code&gt;; it can't connect to a host it made up. &lt;code&gt;open&lt;/code&gt; is full egress, and it's gated behind a verified payment method on purpose.&lt;/p&gt;

&lt;p&gt;Each sandbox is also isolated from every other sandbox. Two agents running on the same physical machine can't see each other, can't reach each other, can't even tell the other one exists. The boundary is the hardware, not a setting the kernel decides to honor.&lt;/p&gt;

&lt;h2&gt;
  
  
  It can pause mid-thought
&lt;/h2&gt;

&lt;p&gt;Agent sessions aren't always a tidy ten seconds. Sometimes an agent does some work, waits on a human to approve a step, then continues an hour later. You don't want to pay for an idle VM during that hour, and you don't want to lose the agent's working state either.&lt;/p&gt;

&lt;p&gt;So a sandbox can be frozen and thawed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node-22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... agent does some work, then hits a checkpoint ...
&lt;/span&gt;
&lt;span class="n"&gt;snapshot_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# frozen to disk, not billed for compute
&lt;/span&gt;
&lt;span class="c1"&gt;# an hour later, maybe from a completely different process:
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pause()&lt;/code&gt; snapshots the entire machine - memory, filesystem, running processes - and stops the clock. &lt;code&gt;resume()&lt;/code&gt; brings it back exactly where it was. The agent doesn't know any time passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Isn't this just a Docker container?"
&lt;/h2&gt;

&lt;p&gt;Fair question, and the honest answer is: no, and the difference is the whole point.&lt;/p&gt;

&lt;p&gt;A container shares the host's kernel. That's fine when you trust the code - it's how most of the internet runs. But the moment the code is something an LLM wrote thirty seconds ago and nobody reviewed, sharing a kernel with the host and with every other tenant becomes an uncomfortable bet.&lt;/p&gt;

&lt;p&gt;Each orkestr sandbox is a real virtual machine. It boots its own kernel, mounts its own filesystem, and runs in its own slice of hardware. A bad command inside the sandbox stays inside the sandbox - there's no shared kernel for it to climb through. You get the disposability of a container with an isolation boundary that holds up when the code is genuinely untrusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What managed sandboxes for AI agents cost
&lt;/h2&gt;

&lt;p&gt;Pay-as-you-go on active vCPU and RAM, billed per second. No per-invocation tax, no minimum, no monthly seat. A quick one-shot call costs a fraction of a cent, and a paused sandbox stops the compute clock until you resume it.&lt;/p&gt;

&lt;p&gt;How many sandboxes you can run at once depends on your plan - 1 at a time on the free &lt;a href="https://orkestr.eu/pricing" rel="noopener noreferrer"&gt;Starter tier&lt;/a&gt;, 5 on Pro, 15 on Team. Sandboxes are in private beta, so the full per-second rates aren't public yet - they land when the beta opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing it to your agent
&lt;/h2&gt;

&lt;p&gt;You don't have to wire any of this up by hand. There's an MCP server - point Claude Code, Cursor, or any MCP client at &lt;code&gt;/api/sandboxes/mcp&lt;/code&gt; and the agent gets tools directly: &lt;code&gt;create_sandbox&lt;/code&gt;, &lt;code&gt;run_shell&lt;/code&gt;, &lt;code&gt;run_code&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;pause_sandbox&lt;/code&gt;, &lt;code&gt;terminate_sandbox&lt;/code&gt;. The agent calls them like any other tool. No glue code.&lt;/p&gt;

&lt;p&gt;The plain REST API also works as a configured sandbox provider for &lt;a href="https://claude.com/blog/claude-managed-agents-updates" rel="noopener noreferrer"&gt;Claude Managed Agents&lt;/a&gt;, so you can keep the orchestration on Anthropic's side and run the actual tool execution on EU hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to actually reach for a sandbox
&lt;/h2&gt;

&lt;p&gt;Sandboxes aren't a replacement for a normal dev environment. They're for a specific shape of problem - mostly "I'm about to run something I don't want on my machine." Six places they earn their keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running model-generated code.&lt;/strong&gt; The obvious one. Your agent writes a script and you want to actually run it to see if it works, not just stare at the diff. A sandbox is what turns "the agent suggested a fix" into "the agent shipped a fix." Worst case is the VM is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touching files you don't trust.&lt;/strong&gt; A user uploads a CSV. A scraper dumps JSON. A teammate shares a notebook from somewhere. You want &lt;code&gt;pandas&lt;/code&gt; on it, but you don't want a Zip-slip surprise on your filesystem. Open it in a sandbox, do the analysis there, return summaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real Linux box from macOS or Windows.&lt;/strong&gt; You're on a Mac. The thing you're testing is Linux-only - a Dockerfile, a glibc bug, a systemd unit. One API call gets you a Linux userland with the right architecture and none of the Docker Desktop dance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel experiments.&lt;/strong&gt; Try three fixes at once. Benchmark four configs. Build against five Python versions. Agents that fan out across sandboxes don't fight over ports, lockfiles, or &lt;code&gt;PATH&lt;/code&gt;. Each one starts clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context-window discipline.&lt;/strong&gt; This one is the subtle agent-design win. If your agent reads a 50,000-row CSV into the conversation, it just burned thousands of tokens on data the model doesn't need to see. If the agent works on it &lt;em&gt;inside&lt;/em&gt; a sandbox and returns &lt;code&gt;df.describe()&lt;/code&gt; or a chart, the model sees the conclusion, not the corpus. The sandbox holds the data; the conversation holds the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive intermediate state.&lt;/strong&gt; Generating credentials, decrypting a file, processing PII for a one-shot calculation. A terminated sandbox is gone - no swap file, no shell history, no &lt;code&gt;/tmp&lt;/code&gt; for the next process to read.&lt;/p&gt;

&lt;p&gt;The other half of the rule: sandboxes are the wrong tool when the working directory &lt;em&gt;is&lt;/em&gt; the point. Editing your repo, running your own test suite, iterating on a feature branch - that all belongs on your machine. The sandbox is for code whose provenance you're not sure of, or an environment that needs to be disposable. If neither applies, you're paying latency for no reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A managed sandbox for AI agents is a disposable, isolated computer your agent can break without consequences - and "managed" means you get it from an API instead of building and babysitting the infrastructure yourself. Network access is something you choose. State can be paused and resumed. The isolation boundary is a real VM, not a shared kernel.&lt;/p&gt;

&lt;p&gt;If your agent runs code - and most useful agents do - it should run that code somewhere that isn't your laptop or your production box. That's the whole job a sandbox does.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a sandbox and a container?&lt;/strong&gt;&lt;br&gt;
A container shares the host's kernel; a sandbox VM brings its own. For trusted code that distinction rarely matters. For code an LLM just generated and nobody reviewed, the VM boundary is what keeps a bad command from reaching the host or another tenant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does my agent's code actually run?&lt;/strong&gt;&lt;br&gt;
On dedicated hardware in the EU - Germany and Finland. Sandbox snapshots, environment variables, and runtime memory stay in the EU. For an EU company sending agent-generated code somewhere, that removes a data-transfer conversation. There's &lt;a href="https://dev.to/eu-sandbox-for-ai-agents/"&gt;more on the EU angle here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if my agent forgets to clean up?&lt;/strong&gt;&lt;br&gt;
Every sandbox has a &lt;code&gt;timeout_seconds&lt;/code&gt; (default 600) and auto-terminates when it's hit, so a crashed or forgetful caller can't leave a VM running forever. The SDK's &lt;code&gt;with&lt;/code&gt; block also terminates the sandbox the moment you leave it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run a server inside a sandbox?&lt;/strong&gt;&lt;br&gt;
Not in the current beta - sandboxes are for running commands and code, not for hosting inbound services. If you want a long-lived app with a public URL, that's a regular &lt;a href="https://orkestr.eu" rel="noopener noreferrer"&gt;orkestr deployment&lt;/a&gt;, not a sandbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get access?&lt;/strong&gt;&lt;br&gt;
Managed sandboxes are in private beta. The waitlist is at &lt;a href="https://orkestr.eu/sandboxes" rel="noopener noreferrer"&gt;orkestr.eu/sandboxes&lt;/a&gt; - it asks what you're building so we can triage who gets in each week.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>EU managed sandboxes for AI agents, in private beta</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Tue, 19 May 2026 17:19:41 +0000</pubDate>
      <link>https://dev.to/ianqqu/eu-managed-sandboxes-for-ai-agents-in-private-beta-3378</link>
      <guid>https://dev.to/ianqqu/eu-managed-sandboxes-for-ai-agents-in-private-beta-3378</guid>
      <description>&lt;p&gt;Your agent suggested &lt;code&gt;rm -rf node_modules &amp;amp;&amp;amp; rm -rf .git&lt;/code&gt; to fix a build error. You laughed, but only because you weren't running it. The thing about agent-generated shell commands is that they look reasonable until they don't, and most of them shouldn't run on your laptop or your production box - they should run inside something disposable, where the worst case is "the sandbox crashes" and not "I'm git-forensicing my own repo on a Friday night."&lt;/p&gt;

&lt;p&gt;So we built managed sandboxes for AI agents. Private beta opening, waitlist live today at &lt;a href="https://orkestr.eu/sandboxes" rel="noopener noreferrer"&gt;orkestr.eu/sandboxes&lt;/a&gt;. EU-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;An HTTP API. Your agent calls &lt;code&gt;POST /v1/sandboxes&lt;/code&gt;, gets back a fresh sandbox ID, then calls &lt;code&gt;/exec&lt;/code&gt; to run shell commands, &lt;code&gt;/files&lt;/code&gt; to read or write files, &lt;code&gt;/pause&lt;/code&gt; and &lt;code&gt;/resume&lt;/code&gt; to snapshot a session and bring it back later. When the agent's done, it calls &lt;code&gt;DELETE&lt;/code&gt; and the whole thing - kernel, filesystem, processes - is gone.&lt;/p&gt;

&lt;p&gt;Each sandbox is a dedicated VM. Not a container. Not a v8 isolate. Each one boots its own kernel, mounts its own rootfs, and lives in its own slice of hardware. Cold start is around 150ms; from a warm pool, under 30ms. When two agents are running in two sandboxes on the same physical host, they cannot reach each other - the boundary is hardware, not a namespace the kernel decides to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an EU sandbox, now
&lt;/h2&gt;

&lt;p&gt;Anthropic shipped &lt;a href="https://claude.com/blog/claude-managed-agents-updates" rel="noopener noreferrer"&gt;Managed Agents&lt;/a&gt;, which lets Claude orchestrate an agent loop while tool execution happens in a sandbox provider you configure. The launch listed four supported providers: Cloudflare, Daytona, Modal, Vercel. All four are headquartered in the US.&lt;/p&gt;

&lt;p&gt;That's a problem if you're an EU company sending agent-generated code somewhere. Standard Contractual Clauses cover transfers in principle, but if you're a Berlin fintech or a Paris insurance startup or a German healthcare company explaining to a procurement team why your agent's working data is processed by a US entity, you have a real conversation ahead of you. The EU sandbox alternative had to exist; we built ours.&lt;/p&gt;

&lt;p&gt;So we did. The orkestr legal entity is in the EU. The hardware is in the EU. Your sandbox snapshots, environment variables, and runtime memory never leave the EU. GDPR DPA on request, signed by the same company that runs the runtime - not a US parent's subsidiary three layers down.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it fits next to what's out there
&lt;/h2&gt;

&lt;p&gt;If you've used &lt;a href="https://e2b.dev" rel="noopener noreferrer"&gt;E2B&lt;/a&gt;, &lt;a href="https://daytona.io" rel="noopener noreferrer"&gt;Daytona&lt;/a&gt;, &lt;a href="https://modal.com" rel="noopener noreferrer"&gt;Modal&lt;/a&gt; sandboxes, or &lt;a href="https://developers.cloudflare.com/sandbox/" rel="noopener noreferrer"&gt;Cloudflare Sandboxes&lt;/a&gt;, the shape is familiar: REST API, Python and JS SDKs, exec / files / snapshot primitives. Here's what the Python SDK looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orkestr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-3.12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(sum(range(1_000_000)))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python /workspace/main.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 499999500000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long-running agent sessions can pause and resume across requests, even across workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node-22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;snapshot_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# ...minutes or hours later, from any process:
&lt;/span&gt;&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also an MCP server you can drop into Claude Code or Cursor, and the REST API works fine as a configured sandbox provider for Claude Managed Agents. If your agent can call a tool, it can call this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the beta
&lt;/h2&gt;

&lt;p&gt;Four templates at launch: &lt;code&gt;python-3.12&lt;/code&gt;, &lt;code&gt;python-3.12-bare&lt;/code&gt;, &lt;code&gt;node-22&lt;/code&gt;, &lt;code&gt;ubuntu-24.04&lt;/code&gt;. Each sandbox sized at 1 vCPU and 1 GB of RAM by default; quick one-shot calls cost fractions of a cent. Three network modes: &lt;code&gt;off&lt;/code&gt; (no egress, the safe default for LLM-generated code), &lt;code&gt;restricted&lt;/code&gt; (allowlist for package registries and common APIs), and &lt;code&gt;open&lt;/code&gt; (full egress, gated behind verified payment).&lt;/p&gt;

&lt;p&gt;Snapshots are native. Resuming on the same host is under half a second; cross-host is a few seconds because the memory file has to fly between data centres.&lt;/p&gt;

&lt;p&gt;The compute runs on dedicated bare-metal hardware in Germany and Finland. Hardware virtualisation on every sandbox, no exceptions. Containers are fine for trusted workloads, but the moment an LLM is generating shell commands you haven't seen yet, sharing the host kernel becomes an awkward conversation. We didn't want to have it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing in private beta
&lt;/h2&gt;

&lt;p&gt;The honest list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPU sandboxes. Not in v1. The CPU product has to work first.&lt;/li&gt;
&lt;li&gt;Persistent volumes across sandbox lifetimes. Use pause/resume for now.&lt;/li&gt;
&lt;li&gt;Custom Docker images as templates. Coming.&lt;/li&gt;
&lt;li&gt;Multi-region routing. Two regions are live; we do not auto-route yet.&lt;/li&gt;
&lt;li&gt;Detailed per-sandbox observability for end users. Building.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hit something not on this list that feels broken, it's probably a bug, so please tell us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing, briefly
&lt;/h2&gt;

&lt;p&gt;Per-second CPU and RAM, no minimums, no per-invocation tax. We're keeping the full price list off the page until the first ten design partners have run real workloads against it. The numbers will move once we see what real usage looks like, and we'd rather quote the final price than walk one back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch (because there's always one)
&lt;/h2&gt;

&lt;p&gt;We're letting a small group in each week. The waitlist asks what you're building and what volume you're expecting, which is how we're triaging. Design partners get a discount on the first three months once paid usage opens, and a direct line to us for SDK feedback.&lt;/p&gt;

&lt;p&gt;If you've been waiting for an EU sandbox for whatever agent you're building, &lt;a href="https://orkestr.eu/sandboxes" rel="noopener noreferrer"&gt;the waitlist is here&lt;/a&gt;. If you have questions before signing up, email me (stefan at orkestr dot eu). I read everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Team workspaces on orkestr, and the ownership-transfer problem</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Mon, 18 May 2026 13:10:00 +0000</pubDate>
      <link>https://dev.to/ianqqu/team-workspaces-on-orkestr-and-the-ownership-transfer-problem-48ki</link>
      <guid>https://dev.to/ianqqu/team-workspaces-on-orkestr-and-the-ownership-transfer-problem-48ki</guid>
      <description>&lt;h2&gt;
  
  
  Introducing the Team Plan on orkestr
&lt;/h2&gt;

&lt;p&gt;Imagine this scenario: You signed up for a Team plan a year ago. Your card is on file. You hired two engineers, invited them, and they've shipped most of the production work since. Now, you're leaving the company.&lt;/p&gt;

&lt;p&gt;On most SaaS platforms, this is the moment you have to open a support ticket: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Hi, can the new tech lead take over the subscription? Also all 12 projects. Also the domains. Also the audit log."&lt;/em&gt; Three business days later, you get an email asking for "verification" of something.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Team plan launched on &lt;a href="https://orkestr.eu" rel="noopener noreferrer"&gt;orkestr&lt;/a&gt; today. The entire flow is built right into the dashboard, and an ownership transfer takes about 90 seconds end-to-end. &lt;/p&gt;

&lt;p&gt;Here is what is shipping today, and why building it was harder than it looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Ships Today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit Team Workspaces:&lt;/strong&gt; Create a team, invite members by email, and switch workspaces from the top bar. Personal projects stay personal; team projects are visible to the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Plan Budgets:&lt;/strong&gt; Your Team plan's 15-project allowance is shared across your personal workspace and the team. The team owner's plan applies to team projects automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-Aware Features:&lt;/strong&gt; Members can see who created each project. To protect infrastructure, members cannot delete team projects—only the owner can. Additionally, the activity feed shows team-tagged actions to all members.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless Ownership Transfer:&lt;/strong&gt; If the owner wants out, they can hand the team (and every project and function inside it) to another member atomically. This requires a quick 1 EUR card verification from the new owner. The old owner's subscription gets canceled, and the new owner's billing cycle seamlessly picks up on the original renewal date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Team is available for &lt;strong&gt;29.99 EUR/month or 299 EUR/year&lt;/strong&gt; via the &lt;a href="https://orkestr.eu/pricing" rel="noopener noreferrer"&gt;Pricing Page&lt;/a&gt;. It features the same EU-hosted infrastructure as every other plan, with no US data transfers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workspace Model
&lt;/h2&gt;

&lt;p&gt;The word "workspace" is heavily overloaded in SaaS, so it's worth specifying how it works here.&lt;/p&gt;

&lt;p&gt;orkestr features exactly two workspace contexts: &lt;strong&gt;Personal&lt;/strong&gt; and &lt;strong&gt;Team&lt;/strong&gt;. An early draft included a third context called "All Workspaces"—a unified dashboard showing everything across every team you are in. After testing it on staging for a week, it was completely ripped out. It made security scoping invisible, and data scoping is a detail that should be explicit, not implicit.&lt;/p&gt;

&lt;p&gt;The active workspace is now part of every backend request. If you are a member of a team and you open the projects page, you will not see the owner's untagged personal projects. This isn't hidden by the UI; the backend simply never returns them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expanding Scope for Plan Limits
&lt;/h3&gt;

&lt;p&gt;The one place where scope expands automatically is plan limits. A project's plan is tied to whoever owns it. If you are on the free tier but working on a project within a paid Team workspace, you get full Team-plan limits on that project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rollbacks&lt;/li&gt;
&lt;li&gt;Custom domains&lt;/li&gt;
&lt;li&gt;Expanded CPU and memory budgets&lt;/li&gt;
&lt;li&gt;IP allowlists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You won't hit any "upgrade to Pro" walls inside a team that is already being paid for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shared Budgets and Role-Aware Counts
&lt;/h2&gt;

&lt;p&gt;The 15-project limit on the Team plan is a single budget split across both personal and team contexts. &lt;/p&gt;

&lt;p&gt;If you (the owner) have 5 personal projects and your team has 8 team projects, you have used 13 of your 15 allowed projects. However, your team members will only see "8 of 15" on their dashboard. Their personal projects do not count against the team's budget, and they shouldn't see how many personal projects you hold. Even leaking the raw count feels like a privacy violation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Total Team Budget: 15 Projects
├── Owner's View:   5 Personal + 8 Team = 13/15 Used
└── Member's View:  8 Team Projects     =  8/15 Used

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of detail that nobody notices when it's right, and everyone notices when it's wrong. The same logic applies to the activity feed: members see team-tagged actions from everyone plus their own personal actions, but never the owner's personal actions. The same goes for domains, add-ons, and deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solving the Invite Flow &amp;amp; Email Mismatch
&lt;/h2&gt;

&lt;p&gt;Invites are entirely email-based. The owner enters an email, the system creates a &lt;code&gt;TeamInvite&lt;/code&gt; row, and the invitee gets a link. Standard practice.&lt;/p&gt;

&lt;p&gt;However, a common, annoying edge case occurs when an invitee already has an orkestr account, but under a different email. For example, they signed up with &lt;code&gt;firstname@gmail.com&lt;/code&gt; for personal use, but you invited their work email: &lt;code&gt;firstname@company.com&lt;/code&gt;. Most platforms either silently match the accounts (which is dangerous, as emails are not identity) or hard-fail and tell the owner to try again.&lt;/p&gt;

&lt;p&gt;orkestr handles this gracefully. The acceptance page displays: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This invite was sent to &lt;code&gt;firstname@company.com&lt;/code&gt;. Your account email is &lt;code&gt;firstname@gmail.com&lt;/code&gt;. Are these the same person?" &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the invitee confirms, the system logs both emails on the membership row and grants access. If they say no, the invite stays open for whoever actually controls that email address. This tiny UX detail eliminated roughly half of the anticipated support tickets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Re-engineering Ownership Transfer
&lt;/h2&gt;

&lt;p&gt;Until recently, if a Team-plan owner wanted to leave a team, they had one option: remove every member, delete every team project, cancel their subscription, and let the data evaporate. That isn’t an ownership transfer; that's burning down the office and leaving.&lt;/p&gt;

&lt;p&gt;Here is how the new, 90-second automated flow works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Owner Requests Transfer:&lt;/strong&gt; The current owner selects a team member and checks a consent box acknowledging they will lose access to all team projects and functions.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Target Notified:&lt;/strong&gt; If the target teammate isn't on a Team plan yet, they are prompted to subscribe. A team cannot be handed over to an account whose plan limits cannot support the existing resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Target Pays 1 EUR:&lt;/strong&gt; This is a Mollie card verification, not a real charge. It serves two purposes: it proves the new card is valid before handing over a subscription, and it satisfies EU consumer protection rules requiring an explicit, attributed action before initiating a recurring charge.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Atomic Swap:&lt;/strong&gt; Once Mollie confirms the verification, a single database transaction reassigns the team, every project, and every function in one go. Either every write succeeds, or the entire transaction rolls back.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Subscription Handover:&lt;/strong&gt; The new owner's billing cycle starts exactly on the day the old owner's subscription would have renewed. The old owner's subscription is canceled, and the 1 EUR verification fee is refunded. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aligning the start date to the original renewal date prevents both parties from double-paying or losing paid days, completely eliminating a common customer service trap.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Not in This Release
&lt;/h2&gt;

&lt;p&gt;To be completely transparent, a few features didn't make the cut for today's launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Automatic Expiry on Pending Transfers:&lt;/strong&gt; Right now, transfer requests sit open indefinitely until someone explicitly cancels or accepts them. A 7-day default expiration will be added soon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Multi-Owner Teams:&lt;/strong&gt; There is a strict limit of one owner per team. Co-ownership introduces entirely different database invariants and security complexities, and we didn't want to ship a half-baked version of it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’ve been waiting for team collaboration features, you can get started right now at &lt;a href="https://orkestr.eu/pricing" rel="noopener noreferrer"&gt;orkestr.eu/pricing&lt;/a&gt;. If you’re already on a Pro plan and want to bring your engineers onboard, you can find the upgrade toggle directly in your workspace settings.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>About Vercel's April 2026 breach: your checklist</title>
      <dc:creator>Stefan Iancu</dc:creator>
      <pubDate>Tue, 21 Apr 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/ianqqu/about-vercels-april-2026-breach-your-checklist-4cji</link>
      <guid>https://dev.to/ianqqu/about-vercels-april-2026-breach-your-checklist-4cji</guid>
      <description>&lt;p&gt;Vercel published a &lt;a href="https://vercel.com/kb/bulletin/vercel-april-2026-security-incident" rel="noopener noreferrer"&gt;security bulletin&lt;/a&gt; on April 19 confirming a breach of its internal systems. On April 20, CEO Guillermo Rauch followed up with the root cause: a Vercel employee used a third-party AI tool called Context.ai. Well, this tool got breached. The attacker pivoted from there into the employee's Vercel Google Workspace account, and from there into Vercel environments and environment variables that weren't marked as "sensitive." Is this the customer fault? (jk)&lt;/p&gt;

&lt;p&gt;Vercel says a "limited subset" of customers were directly impacted and have been contacted. They also say Next.js, Turbopack, and their other open-source projects have been analyzed and are believed safe.&lt;/p&gt;

&lt;p&gt;That's the facts. Now the honest part: if you deploy on Vercel, don't wait for an email. Rotate first, ask questions later.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Vercel breach checklist&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Rotate every environment variable that wasn't marked as "sensitive" in Vercel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do this first if you do nothing else. Vercel's &lt;a href="https://vercel.com/docs/environment-variables/sensitive-environment-variables" rel="noopener noreferrer"&gt;sensitive environment variable feature&lt;/a&gt; encrypts values at rest and hides them from the dashboard after creation. Everything else - the default - was readable by anyone with access to that employee's Google Workspace session. Assume those values leaked. Payment processor keys, database URLs, auth secrets, cloud provider keys, admin API tokens to third-party SaaS. Rotate all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Review your GitHub organization audit log.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your Vercel account is connected to GitHub (it almost certainly is), pull the audit log for April 1 through today. Look for unexpected OAuth grants, new deploy keys, workflow edits, branch protection changes, or pushes from unusual IPs. The community-maintained IR playbook has a solid filter list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. If you use the Vercel ↔ Linear integration, check Linear's audit log too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Linear issues are where people paste stack traces, database URLs, and occasionally plaintext secrets while debugging. The integration has read access to issues. Workspace settings → Security → Audit log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Audit any npm package you publish from a Vercel-connected workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vercel says their own packages are clean. That's their release path, not yours. If you use Vercel's build environment to run npm publish or push to a registry, treat any package published in the exposure window as suspect until you've diffed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Don't skip the boring step: inventory your exposure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every Vercel team you control, list the projects, the connected Git orgs, the integrations, and the humans with access. Half the pain of incident response is discovering on day three that there was a second team nobody remembered.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The part nobody wants to talk about&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The attack didn't start at Vercel. It started at &lt;a href="https://context.ai/" rel="noopener noreferrer"&gt;Context.ai&lt;/a&gt;, an AI tool a single employee had connected to their work account. They also &lt;a href="https://context.ai/security-update" rel="noopener noreferrer"&gt;posted&lt;/a&gt; about it. This is how modern breaches happen. You don't get popped through your WAF. You get popped because someone on your team authorized an OAuth grant to a YC-stage AI startup that had one security engineer and a Notion doc for an incident response plan.&lt;/p&gt;

&lt;p&gt;There's no EU-vs-US angle here. European PaaS vendors have employees too. Employees use AI tools. The same thing could happen to Scalingo, Clever Cloud, or us at orkestr. &lt;/p&gt;

&lt;p&gt;What matters is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much blast radius does one compromised employee account actually have?&lt;/li&gt;
&lt;li&gt;Are production secrets readable from a dashboard, or are they sealed?&lt;/li&gt;
&lt;li&gt;How fast does the vendor tell you, and how specifically?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vercel's bulletin on April 19 was terse to the point of unhelpful. The detail came from press reporting and a follow-up from the CEO a day later. "We recommend reviewing environment variables" isn't incident communication. It's a legal posture. You should expect better from anyone you hand your deploy keys to — us included.&lt;/p&gt;

&lt;p&gt;Whatever platform you're on: rotate your secrets today. Mark them sensitive. Cut the SaaS integrations you're not using. And write down who on your team has production access, because there will be a next one.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>news</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
