AI-Generated Code: Why You Still Need to Think
AI can write code, but it cannot choose your system’s trade-offs. Learn to review generated code, design useful benchmarks, and build reliable software.

AI Can Write the Code. You Still Own the Decisions.
AI coding agents can rewrite an application in another language, generate a working prototype, and churn out enough code for a demo to look convincing. That's useful. It's also easy to confuse code generation with engineering.
The hard part of a rewrite isn't translating syntax. It's deciding what the new system must preserve, how it should behave under load, and which failures you're willing to accept. Those choices don't vanish just because an agent wrote the implementation. They're still made - usually indirectly - by whoever wrote the prompt, by library defaults, or by the generated code itself.
A rewrite of a production application across Ruby, Rust, and Elixir makes the point concrete. Different versions will pick different compatibility, concurrency, and delivery approaches, so a benchmark won't automatically compare like-for-like systems. The question isn't "which language is faster on this test?" It's what the test actually measures, what the implementation gives up, and whether those trade-offs match your product.
This article walks through how to evaluate AI-generated code, interpret performance results, and make reliability decisions before a benchmark or code review gives you false confidence.
Generated code still contains product decisions
A prompt like "rewrite this application in Rust" sounds specific. It isn't. It names a programming language and leaves almost all the product and operational requirements unstated.
Should the new version preserve every existing behavior? Is it okay to remove a feature or change an API? Do you optimize for lower latency, higher throughput, or predictable behavior during overload? How much memory is the system allowed to use? If a process crashes, can it lose notifications that were waiting to be delivered?
None of those decisions get magically avoided. If the prompt doesn't answer them, the generated code will still choose. It might be fine for a demo and a mess for production. And it can be hard to notice because everything works "normally" until it doesn't.
A rewrite is not just a language translation
Take a chat application rewritten from a mature web framework. One implementation might closely preserve existing request behavior. Another might drop a security token just to make responses easier to cache. One might keep Redis for queues; another might switch to in-process messaging. Those choices affect compatibility, failure behavior, and operations - not just performance.
So a direct language comparison can mislead you. If two implementations differ in features, queueing, caching, or delivery guarantees, you can't automatically credit the performance difference to "the language." The systems are doing different work.
A useful rewrite brief should spell out the constraints that matter, including:
- Compatibility: Which routes, data formats, and user-visible behaviors must stay the same?
- Reliability: Which events may be retried, delayed, duplicated, or lost?
- Performance: Are you targeting throughput, tail latency, or consistent response times?
- Resources: What are the memory and CPU limits in the target environment?
- Operations: What should happen when a database, queue, or client falls behind?
This is especially important with an AI agent. A model can produce a coherent implementation without reliably figuring out which assumptions your existing system depends on. It can't read business consequences from a repository the way your team does.
Code review is still part of the work
AI-generated code can compile, pass a happy-path test, and still mishandle concurrency. For instance, a service might process database reads sequentially when they could run concurrently. Another implementation might do blocking database work inside an async task.
In an async runtime like Tokio, tasks generally cooperate by yielding control. If a task blocks a worker thread while waiting for I/O, other tasks scheduled on that same thread may stall. The impact depends on the runtime, the workload, and the implementation - but the takeaway is simple: using an async language or framework doesn't automatically make every operation non-blocking.
Locks deserve the same level of scrutiny. A synchronous lock held briefly can be fine. Held for longer, it can block other work sharing a runtime thread. Choosing an async lock doesn't "fix" the design by itself either; it changes the behavior and overhead, and the critical section still needs to stay small.
The practical lesson isn't "review everything like it's an exam." It's that the parts affecting concurrency, data integrity, and failure behavior are worth review by someone who understands the runtime. AI can speed up implementation. It can't take responsibility for what happens next.
A benchmark measures a workload, not a language
A benchmark only tells you something if it's designed to answer the question you're actually asking. A single "requests per second" number won't tell you whether the system delivers notifications, keeps latency within reason, or stays inside its memory budget.
In one reported comparison of chat application rewrites, a closed-loop test sent a new request only after a client received a response to its previous request. That answers a useful question: how much work can a fixed set of clients complete when each waits for its own result?
But it doesn't represent every kind of real-world load.
Closed-loop and constant-arrival-rate tests answer different questions
In a closed-loop test, clients slow down automatically when the application slows down. The offered request rate therefore depends on the system's response time. That can be useful for studying capacity with a fixed number of active clients - but it can hide what happens when requests keep arriving while the application is already struggling.
A constant-arrival-rate test sends requests at a defined rate regardless of whether earlier requests have finished. That can better model bursts of independent user actions, scheduled jobs, or incoming events. It can also expose queue growth and timeouts that a closed-loop test may never trigger.
Neither method is universally "better." The right choice depends on the production behavior you're trying to model. If your users are basically "waiting for each page load," that's a different scenario from testing a constant stream of incoming events.
And when the benchmark is about notification delivery, throughput alone isn't enough. You need to know how many events were produced, how many arrived, how late they arrived, and what happened when clients fell behind. Comparing one system's successful delivery rate to another without checking request volume and error rates can produce a headline that's technically correct and practically misleading.
Measure the service users actually experience
Useful measurements depend on the product, but under load a system is usually better described by a set of outcomes rather than a single score:
- Throughput: How much work completes per second?
- Request errors: How many requests fail or time out?
- Delivery rate: How many expected events reach their destination?
- Latency distribution: Typical and worst-case delays, including p95 or p99.
- Resource use: How much CPU and memory the workload consumes.
- Recovery behavior: Does the service catch up, disconnect slow clients, drop work, or crash?
For example, the test report from the comparison mentioned earlier used a constant rate of 100 POST requests per second. The Rust version delivered a relatively small share of notification events and didn't show the same HTTP timeout rate. The Elixir version delivered more events in that test, but a significant share of HTTP requests timed out. None of that identifies a universal winner. It just shows different behavior under one particular workload and set of implementation choices.
So when you benchmark your own system, start by defining the success condition users care about. If your promise is "every notification arrives promptly," measure that. If clients can recover by reconnecting and fetching current state, test that recovery path. A benchmark should support a decision, not turn your product into a scoreboard.
Backpressure makes reliability a design choice
When one part of a system produces work faster than another part can consume it, something has to give. The system can queue it, reject it, drop it, slow the producer down, or run out of resources. That's backpressure - and each option has a cost.
A broadcast channel makes the trade-offs concrete. One new chat message might need to reach many connected clients. A bounded channel limits how many events can wait in memory. If a receiver falls too far behind, the channel can report lag. At that point, the application has to pick a policy: disconnect that client, let it recover through a fresh request, or try to retain more events.
In the reported Rust implementation, the broadcast capacity was set to 256. Increasing it to 16,384 improved event delivery at the tested request rate. But the bigger buffer also pushed worst-case delivery latency from seconds into well over a minute. More messages were retained, but some clients waited much longer for them.
That isn't automatically "better." A stale notification can be less useful than a clean disconnect followed by a refresh. In a chat app, a browser that reconnects and fetches the latest messages may recover better than one that silently waits on an old event. What's "correct" depends on how the client behaves and what users expect.
Unbounded queues do not remove the limit
Some systems use mailboxes or queues that can grow without a configured bound. That can make producers look more resilient because they aren't forced to reject work immediately. It doesn't create unlimited capacity. The limit just moves to the machine's memory.
In the same reported stress test, the Elixir version used substantial memory as load increased. That doesn't mean the language or its process model is inherently unsafe. It means queue growth and memory use need to be measured and constrained in the application you're testing. Any system can fail once its workload exceeds capacity; the failure mode depends on the design.
The key decision isn't "dropping work is always worse than buffering it." The key decision is which outcome is less harmful for your product:
- Drop an event and let the client recover by fetching current state.
- Keep events in a bounded queue and disconnect clients that fall behind.
- Buffer more, accepting increased latency and memory use.
- Reject or slow new requests before the system becomes unhealthy.
Each policy makes a different promise. A reliable system isn't one that never loses work under any circumstances. It's one where the limits and recovery behavior are understood, tested, and aligned with what users need.
A practical review framework for AI-assisted rewrites
You don't have to reject AI-generated code or inspect every line with equal intensity. You do need a process that keeps a plausible implementation from quietly redefining your product.
1. Write down the invariants
Before asking an agent to rewrite the system, list what must stay true. Include API behavior, security requirements, data integrity, delivery guarantees, and operational limits. Split non-negotiables from choices you're actually willing to change.
For example, notification delivery might be best-effort if clients can reliably fetch the latest state after reconnecting. It might require durable delivery if notifications trigger an irreversible business process. Same general mechanism. Different consequences.
2. Ask the agent to expose its assumptions
Don't ask only for code. Ask for a list of assumptions and decisions - especially around concurrency, queues, retries, caching, and failure recovery. Treat that output as a review aid, not as proof that everything is correct.
Compare those assumptions to your invariants. If the agent changed compatibility, removed a dependency, or swapped in an in-memory queue, make the call explicitly: is that acceptable for your product?
3. Inspect the risk-heavy paths
Focus review where a bug can cause real harm: data loss, security issues, resource exhaustion, or big latency spikes. Typical places include:
- Database operations and transaction boundaries
- Async tasks, blocking calls, and lock usage
- Queue limits, retry policies, and duplicate handling
- Authentication, authorization, and request validation
- Client reconnection and state recovery
A passing test suite helps, but it only covers what the tests exercise. For an overview of practical review techniques, tie this process to your team's code review checklist.
4. Design the benchmark around production behavior
Write down the workload before you pick a tool or publish results. Specify request rate, number of clients, test duration, data size, and what counts as success. Decide whether you're testing closed-loop behavior or a constant arrival rate.
Measure more than throughput. Include error rates, event delivery, latency distribution, and memory use. Run enough repetitions to separate real effects from noise. Keep the software versions and configuration available so someone can reproduce the comparison.
5. Choose failure behavior deliberately
Set queue and buffer limits. Decide what happens when those limits are hit. Test slow clients, database delays, process restarts, and traffic spikes. If the system disconnects a client, verify the client can recover. If it retains work, verify the queue can't grow until the machine runs out of memory.
This isn't a call for perfect foresight. It's a way to make the important choices visible while you can still change them. An imperfect system with known limits is easier to operate than a "clean" implementation whose failure behavior is an accident.
Conclusion: keep the human in the engineering loop
AI agents can save time on implementation. They don't remove the need to define requirements, review the parts that matter, and decide what the system should do under pressure.
A benchmark isn't a verdict on a programming language. It's evidence about a particular implementation, configuration, and workload. Read it alongside error rate, latency, delivery behavior, and resource use - and make sure those measures reflect the product you're building.
Before your next AI-assisted rewrite, document the invariants, ask for assumptions, test realistic load, and choose failure behavior deliberately. If you have a benchmark or generated implementation in front of you, start by asking what it actually measures. That question is often more useful than asking which language "won."