Dev.to WebDev 🛠 Dev 👁 0 📖 8 min read

Why I Built an AI Agent in Native PHP in 2026 (And Not Python/Node)

Every time I tell another developer that I built an AI agent in PHP, the reaction is almost always the same: raised eyebrows, a thin smile, then the question — "seriously?" Yes, seriously. I built ZixcAI — an AI agent

Every time I tell another developer that I built an AI agent in PHP, the reaction is almost always the same: raised eyebrows, a thin smile, then the question — "seriously?"

Yes, seriously.

I built ZixcAI — an AI agent that can execute code in a sandbox, read files, fetch the web, analyze images, and maintain long-term memory — entirely in native PHP, no framework. Not Laravel, not Symfony, not Node.js, not Python. PHP 8.3, SQLite, a single process.

This is not a "PHP is better than X" post. This is a post about why, for this specific use case, PHP turned out to be the right call — and what I learned while shipping 2000+ lines of AI agent code in a language most people consider "ancient" for AI work.

Context: What Am I Actually Building?

ZixcAI is an agentic AI product. When I say "agentic", I don't mean a chatbot that returns greetings. I mean an LLM that can:

  • Execute code — Python scripts, shell commands, in a per-user sandbox
  • Read and write files — in an isolated workspace
  • Fetch web content — with host validation and response size limits
  • Search the web — via a bridge to Serper/Tavily
  • Analyze images — routed to a vision sub-agent
  • Remember you — persistent memory across sessions
  • Orchestrate multiple tools — in a single turn, with retries and circuit breakers

The chat interface streams responses via Server-Sent Events (SSE). Every tool call is logged, cached, and auditable. There is a full user system, session management, CSRF protection, attachment storage, and a subscription model.

This is not a weekend project. It's a production system.

The Reflex: "Shouldn't This Be Python?"

The standard argument goes: AI = Python. LangChain, LlamaIndex, CrewAI, Autogen — everything in the ecosystem is Python. If you're building an AI product, you should be in Python.

Here's the thing though — PHP is not competing with Python here. PHP is competing with the framework layer.

The actual work of an AI agent is:

  1. Receiving HTTP requests — a chat message, a tool result, a stream.
  2. Managing state — user sessions, conversation history, permissions.
  3. Orchestrating API calls — sending prompts to an LLM, parsing responses, handling tool calls.
  4. Streaming output — pushing deltas to the browser in real-time.
  5. Persisting data — messages, turns, tool calls, memories.

Python does #3 well. But #1, #2, #4, and #5? PHP was literally designed for this. The entire web runs on request-response cycles that PHP has been optimizing for two decades.

When I stripped away the "should be Python" reflex and looked at what the agent actually does, PHP wasn't just adequate — it was faster to ship, easier to deploy, and cheaper to run.

Why PHP Actually Made Sense

1. Deployment Is Trivial

Python deployment is a small war: virtual environments, dependency pinning, WSGI servers, process managers, reverse proxies. Multiply that by every environment (dev, staging, prod) and you've got hours of ops work before writing a single line.

PHP deployment: copy files, point a web server at them, done. Every shared host in the world runs PHP. My entire production deploy is git pull and a cache clear.

For a solo developer shipping fast, this is not a nice-to-have. It's the difference between launching this month or this quarter.

2. The Request Lifecycle Fits the Agent Workflow

An AI agent turn is: request comes in → orchestrate a bunch of sub-requests → stream the response back → persist state → done. There's no persistent "agent daemon" that stays alive between turns.

This is exactly what PHP's shared-nothing request model was designed for. No memory leaks across requests. No concurrency primitives to fight. No event loop to debug. Each turn is a fresh, isolated, stateless execution.

Python's async model is powerful, but for this specific shape of work, it's also overkill and easier to get wrong.

3. SQLite + WAL = Zero-Ops Persistence

I chose SQLite with Write-Ahead Logging for storage. No Postgres to manage, no separate cache layer, no Docker compose stack. On a single-node agent, SQLite with WAL handles more than enough concurrent reads and writes.

The full-text search feature? SQLite has FTS5. Conversation search, message search, all indexed. No Elasticsearch. No Meilisearch.

Combined with PHP's stable PDO layer, persistence is a solved problem in ~30 lines of code.

4. SSE Streaming Is Actually Clean in PHP

Real-time streaming responses usually mean WebSockets or SSE, and both feel awkward in traditional web stacks. In PHP, they're just... responses that keep echoing.


php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');

while (ob_get_level() > 0) ob_end_clean();
@ob_implicit_flush(true);

echo "event: delta\n";
echo "data: " . json_encode(['text' => $chunk]) . "\n\n";
@ob_flush();
flush();
That's the entire streaming contract. No framework, no library, no adapter. The main gotcha is output buffering — you have to disable it aggressively or the browser will buffer your entire stream before rendering. Once you know that, SSE in PHP is 20 lines of boilerplate.

5. The Framework Tax Is Real
I built this in native PHP, not Laravel. Here's why: frameworks are great when you need to solve 50 problems at once. When you need to solve 5 specific problems well, frameworks add more abstraction than value.

Every agent turn passes through the same 4-5 functions. My tool execution layer is ~200 lines. My memory system is ~150 lines. My SSE handler is ~80 lines. If I had used Laravel, half my debugging time would be understanding the framework, not my own code.

This is not a critique of Laravel. It's an observation that for a small, well-understood domain, a framework is sometimes the wrong abstraction.

The Trade-Offs (Because Nothing Is Free)
Let me not pretend this was all rosy. PHP has real limitations for this use case:

Concurrency. PHP-FPM spawns processes. If you need 100 concurrent long-running requests, you need 100 processes. For a chat product this is fine because most requests are short-lived streams, but if you were building a real-time collaboration tool, this would hurt.

Long-running background jobs. PHP doesn't have a natural "worker daemon" story without extra infrastructure (Supervisor, PM2, etc.). For ZixcAI, background work is minimal — title generation and memory extraction run inline after each turn. If I needed serious async processing, I'd probably add a small worker process in Node or Python.

Python ecosystem. There are Python libraries for AI that have no PHP equivalent. The OpenAI SDK for Python is richer than any PHP option. But the OpenAI API is just HTTP + JSON — you don't need an SDK. A curl_init() and a json_decode() cover 99% of what the SDK does.

Hiring. If I ever need to hire, "PHP developer with AI experience" is a smaller pool than "Python developer with AI experience." But for a solo project, this is a hypothetical.

What I Learned Building This
1. Streaming Is a Debugging Nightmare Until You Understand Buffering
The first version of my SSE stream didn't work. The browser received nothing until the entire response completed, then dumped everything at once. Three days later I found out that output_buffering in php.ini was silently holding every chunk.

Lesson: In PHP, for streaming, ob_end_clean() + ob_implicit_flush(true) + flush() after every chunk is non-negotiable.

2. Upstream Timeouts Kill Chat Products
LLM APIs are slow. A single turn can take 30+ seconds if the model is reasoning heavily. If your HTTP timeout to upstream is 15 seconds, you'll get random "chat failed" errors that only happen sometimes.

Lesson: Set CURLOPT_TIMEOUT to 0 (infinite) for streaming, and set a hard wall-clock limit at the product level (e.g., "if no token received for 45 seconds, abort the turn"). Distinguish between transport timeout and product timeout.

3. Tools Are Just Functions With a Contract
I spent a week designing an elaborate tool-call protocol before realizing: the LLM only sees a JSON schema and returns a JSON call. Your job is to route the call, sanitize arguments, execute safely, and return a text result.

php
if ($tool === 'shell.exec') {
    $cmd = (string)($args['command'] ?? '');
    if (!self::isSafeShellCommand($cmd)) {
        return ['exit_code' => 400, 'stdout' => '', 'stderr' => 'blocked'];
    }
    // ... execute via bridge
}
That's the whole tool layer. Everything else is bookkeeping.

4. Memory Is Harder Than Tools
Reading files and running code is easy — the LLM tells you what to do, you do it. But knowing what to remember across sessions is a genuinely hard problem. My current system extracts 0-3 memory items per turn via a secondary LLM call, deduplicates with similarity matching, and injects the top-30 into the system prompt.

It works. It's not perfect. But it taught me that "memory" in agentic systems is 20% storage and 80% extraction policy.

5. Circuit Breakers Save You From Cascading Failures
If your sandbox bridge goes down, every tool call will hang for 30 seconds and then fail. That's a bad user experience and it exhausts your process pool.

Adding a simple circuit breaker (5 consecutive failures → short-circuit for 30 seconds) made the product dramatically more resilient:

php
if (self::$bridgeFailures >= self::BRIDGE_FAILURE_THRESHOLD) {
    if (time() - self::$lastBridgeFailure < self::BRIDGE_CIRCUIT_RESET_SECONDS) {
        return ['ok' => false, 'status' => 503, 'body' => [
            'violation_code' => 'bridge_circuit_open',
        ]];
    }
}
This is 6 lines of code and it's the difference between "the product degrades gracefully" and "the product crashes when one dependency hiccups".

So, Should You Use PHP for Your AI Agent?
If you're building a research project, a training pipeline, or heavy ML work — no. Use Python. The ecosystem is unmatched.

If you're building an agentic product where the LLM does the reasoning and your job is orchestration, streaming, and persistence — PHP is a perfectly defensible choice. It's fast to ship, cheap to deploy, and boring in the best way.

The "AI must be Python" rule is a community convention, not a technical constraint. The technical constraint is: can your language make HTTP requests, handle JSON, and stream output? If yes, you can build an AI agent in it.

The real question isn't "is PHP good for AI" — it's "is this stack right for this product". For ZixcAI, it was.

What's Next
I'm continuing to ship ZixcAI features: better memory, multi-modal inputs, and more tool primitives. If you want to see what an agentic AI built on PHP actually looks like in practice, check it out.

If you're building something similar — in PHP, Python, or anything else — I'd love to hear about it. Drop a comment with your stack and what surprised you the most.

And if you made it this far: yes, PHP is fine. Really.

Thanks for reading. If you found this useful, I write occasionally about AI engineering, unorthodox stack choices, and building products solo.
📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.