Every AI agent framework you’ve heard of is written in Python — LangChain, LangGraph, AutoGen, CrewAI, Semantic Kernel (partially). When we started building an enterprise AI agent platform in Go at StackGen, the obvious choice was Python.

We chose Go instead. Several months of production development later, here’s why — and the trade-offs we had to accept.

Update (July 2026): The trade-off in §5 — Dynamic LLM JSON parsing is softer than when this post first published. We now route shared LLM JSON decode paths through github.com/kaptinlin/jsonrepair — a Go port of Jos de Jong’s jsonrepair, tuned for malformed model output. See the revised section below.


The Problem We Were Solving

We weren’t building a chatbot. We were building an agent runtime — software that lets AI models invoke shell commands, call APIs, manage infrastructure, and delegate work to sub-agents. In production. On your servers. With your credentials.

The requirements:

  1. Run many agents concurrently per deployment, each with its own tools, memory, and policies
  2. Embed as a library inside a larger platform (Aiden), not run as a separate service
  3. Deploy as a single binary to air-gapped environments, edge nodes, and developer laptops
  4. Low overhead per tool call — agents call many tools per task; middleware cost compounds
  5. Handle untrusted input — agents process user prompts that might contain injection attacks

Let’s walk through how Go handles each of these.


1. Concurrency: Goroutines Are the Perfect Model for Agents

An AI agent in production does many things at once:

  • Calls an LLM API and waits for streaming tokens
  • Executes a shell command on a remote server
  • Queries a vector database for relevant context
  • Listens for human approval on a pending tool call
  • Runs scheduled work that triggers another agent

In Python, you’d use asyncio — which means every function in the call chain must be async, every library must support it, and debugging involves staring at coroutine tracebacks. Or you use threading, and then you’re fighting the GIL.

“But Python has No-GIL now!” Fair — Python 3.13+ introduced free-threaded mode, and asyncio.TaskGroup acts like Go’s structured concurrency helpers. But even with No-GIL, Python still suffers from “colored function” syndrome: async functions can’t call sync functions without an executor, sync functions can’t call async without an event loop. Your entire dependency tree must agree on a color. In Go, concurrency is colorless — every function can be spawned as a goroutine, period.

In Go, parallel work is just functions running concurrently with structured error collection — no event loop, no GIL, no async coloring through the entire dependency tree.

Real example: When an agent delegates to several sub-agents in parallel, each runs with its own model session, tool set, and memory context. The parent waits for all of them; if one fails, the context cancels and the others wind down cleanly.

What about Python’s asyncio?

It works. But it infects your entire codebase. If your ORM isn’t async-compatible, you’re blocking the event loop. If a library uses blocking HTTP instead of async HTTP, you need a thread pool executor. In Go, everything is concurrent by default — net/http, database drivers, file I/O — because goroutines are how the language works, not an opt-in mode.


2. Single Binary: Ship One File, Not a Dependency Tree

Our agent runs on developer laptops, Kubernetes clusters, air-gapped enterprise servers, and CI/CD pipelines as a CLI tool.

With Go: cross-compile to a single statically linked binary, copy it to the target machine, done. No Python interpreter, no pip, no virtualenv, no requirements.txt, no “but it works on my machine.”

“Just use Docker / PyInstaller / uv!” Python bundlers create bloated self-extracting archives, not true machine-code binaries. A minimal Python Docker container with a popular agent framework and its dependencies easily exceeds a gigabyte. Our Go binary is a fraction of that size, statically linked, no runtime needed.

For enterprise customers running in regulated environments, “install Python, pip, and a hundred packages” is a non-starter. “Copy this binary” is a conversation.


3. Embed as a Library, Not a Service

This is the decision that sealed it.

Our agent runtime needs to run inside our orchestration platform (Aiden), which uses Temporal for durable workflow orchestration. Temporal is itself built in Go, with a first-class Go SDK. The agent runtime runs as part of the platform process — not a sidecar, not a subprocess, but a library import in the same binary.

Same process, same memory space, shared types. No serialization overhead, no network hops, no container orchestration for the agent itself. If the agent runtime were Python, we’d need gRPC bridges, subprocess management, or a sidecar container — each adding latency, deployment complexity, and an entire class of serialization bugs.

In Python, you’d either run the agent as a subprocess, import it as a package (with version conflict risk across large projects), or run it as a separate service (network overhead). Go modules give you versioned, reproducible dependencies with a single module file.


4. Compile-Time Safety: Catch Breaking Changes Before Production

We have a large codebase organized around small interfaces — every method takes a context and a request struct, returns typed results. When we change an interface, the compiler immediately tells us every place that needs updating.

“But we use Pydantic and mypy!” Yes, Python has type-checking now, and Pydantic is excellent. But Pydantic is runtime validation — it adds CPU overhead on every model instantiation and doesn’t catch mismatches until the code actually executes. mypy is compile-time-ish, but it’s optional and many teams don’t enforce it in CI. Go gives you type safety natively at compile time with zero runtime cost. If it compiles, the types are correct.

We generate test doubles from interfaces automatically. These fakes are type-safe — if the interface changes, the fake won’t compile, and every test using it fails with a clear compiler error, not a runtime surprise.


5. Performance: When Every Tool Call Counts

Agents are chatty. A single task might involve many tool calls. Each call passes through a middleware stack — logging, audit, loop detection, approval gates, timeouts, rate limits, and more.

In Go, middleware is function closures — lightweight per call. For a single agent, that overhead is noise compared to LLM latency. The real performance win is memory footprint at scale.

Goroutines start with a tiny stack that grows as needed. Running many concurrent agents in Go consumes dramatically less RAM than the same workload in Python — where each agent loads framework code, validation libraries, tokenizers, and an async event loop. On edge nodes and developer laptops with memory limits, that’s the difference between “it runs” and “it thrashes.”


The Trade-offs We Accepted

It’s not all sunshine. Here’s what we gave up:

1. Smaller AI/ML ecosystem

Python has HuggingFace, PyTorch, scikit-learn, and thousands of AI libraries. Go doesn’t.

How we handled it: We don’t run ML models. We call LLM APIs via HTTP. The heavy ML work happens on the provider’s infrastructure. Our Go code handles orchestration, governance, and tool execution.

It’s worth noting that while Go lacks ML modeling libraries, AI infrastructure is already heavily Go-based: Ollama, Kubernetes, Docker, and Temporal are all written in Go. Models run in Python/C++/Rust; the infrastructure layer is Go’s sweet spot.

2. Fewer agent framework examples

Every “build an agent” tutorial is in Python. Our team had to translate concepts, not copy code.

How we handled it: This was actually a feature. Translating forced us to understand the algorithms deeply rather than cargo-culting. When we implemented ReAcTree, we found production bugs that paper-to-Python implementations would have hidden behind duck typing (more in a dedicated post).

3. Prototyping speed

Python is faster for throwaway experiments. Go requires more upfront structure.

How we handled it: We accepted slower early iterations in exchange for faster late-stage development. By month three, type-safe interfaces and auto-generated fakes meant new features had fewer bugs on first commit than our Python prototypes did after a week.

4. Hiring

More ML engineers know Python than Go.

How we handled it: We’re building infrastructure, not ML models. Systems engineers who know Go are exactly the profile we need. Go’s simplicity means Python developers ramp up in a few weeks.

5. Dynamic LLM JSON parsing

This used to be Go’s genuine pain point for AI work. LLMs return subtly malformed JSON — fences, single quotes, trailing commas, truncated objects. Python’s json.loads() swallows arbitrary JSON effortlessly. Go’s encoding/json forces you to unmarshal into strict structs.

How we handled it (2026 update): We still use strict structs for typed boundaries — that’s a feature, not a bug. But for syntax problems models create, we centralized repair in github.com/kaptinlin/jsonrepair. Shared decode helpers try strict parsing first; when that fails, they repair and retry.

We still layer other tactics where semantics matter: deferring argument parsing until the tool handler knows its schema, path-based extraction when we need specific values without full structs, and re-prompting the model when JSON is syntactically valid but semantically wrong (repair can’t fix wrong shapes).

It’s more ceremony than json.loads() in a REPL. The upside is unchanged: when JSON parses into your struct, you know it’s structurally valid. See the JSON repair post for why one repair pass isn’t enough in production.


When You Should NOT Use Go for Agents

Be honest about when Python is the right choice:

  • You’re prototyping and need to test ideas in hours, not days
  • You’re running local models with HuggingFace/PyTorch and need direct GPU access
  • Your team is ML-first and everyone thinks in NumPy
  • You’re building on LangChain/LangGraph and the ecosystem matters more than performance
  • You’re a solo developer and can’t invest in the upfront structure Go requires

Go is for when your agent system is infrastructure — long-running, multi-tenant, governed, deployed across environments. If that’s not your situation, Python is genuinely the better choice.


The Scorecard

Criterion Go Python
Concurrency model Native, colorless goroutines asyncio (opt-in, colored functions)
Deployment Single binary, cross-compile Interpreter + venv + many packages
Library embedding Module import, same process Possible but fragile at scale
Type safety Compile-time, zero-cost Pydantic (runtime) + mypy (optional)
Memory footprint at scale Low High
Middleware overhead Very low per call Higher at scale
Dynamic JSON parsing Strict structs; repair libraries help json.loads() handles most syntax
AI/ML ecosystem Minimal (strong AI infra) Dominant
Prototyping speed Slower start Rapid iteration
Hiring pool (ML) Smaller Larger

What’s Next

In the next post, I’ll cover why we chose TOML over YAML and PKL for agent configuration — and why the config format you pick matters more than you think.



Acknowledgments. Built with the StackGen Aiden team — the engineers behind the agent runtime and platform this series describes.

If you’re building agents in Go, I’d love to hear about your experience. Find me on GitHub or LinkedIn.


🚀 We’re building AI-powered SRE at StackGen. If you’re tired of 3 AM pages and want AI agents that triage incidents, run diagnostics, and draft RCA reports — check out ai.stackgen.com and try our new SRE offering.