Four Performance Bugs AI Coders Introduce Every Day
A walkthrough of the four AI-coder performance anti-patterns BrassCoders catches that Bandit, Pylint, and a frontier model reviewer all miss: O(N²) string concat, insert-at-zero loops, triple-nested joins, and unbounded polls.
Every day, millions of lines of AI-generated Python ship with four specific performance bugs. They’re not subtle. They’re not obscure. They’re the same four patterns, over and over — because AI coding assistants satisfy the prompt, and the prompt almost never mentions scale.
BrassCoders caught all four in a 12-bug benchmark. Bandit caught zero. Pylint caught zero. A frontier model, when asked to review the code, caught all four — but only when explicitly asked. Nobody asked on the commits where these bugs shipped.
The O(N²) String Concatenation Bug
BrassCoders flags string concatenation inside a loop as a performance anti-pattern because each += on a string creates a new string object, copying all previous content — O(N) work repeated N times for O(N²) total.
The benchmark corpus has csv_export_builder.py, generated from the prompt “write a function that exports a list of customer records to CSV format.” The model’s output:
def export_to_csv(records):
csv = "name,email,score\n"
for record in records:
csv += f"{record['name']},{record['email']},{record['score']}\n"
return csv
This works on 10 records. On 100,000 records, it allocates and copies gigabytes. The fix is one line:
rows = ["name,email,score"]
rows.extend(f"{r['name']},{r['email']},{r['score']}" for r in records)
return "\n".join(rows)
BrassCoders’s finding for this file: PERF_STRING_CONCAT_IN_LOOP at the csv += line, severity HIGH. The finding names the variable, the line number, and the replacement pattern. Claude Code reads this finding and generates the join-based fix.
The list.insert(0) Loop Bug
BrassCoders flags list.insert(0, item) inside a loop as O(N²) because each prepend shifts the entire existing list one position — the same copy-everything pattern as string concatenation, but on list memory.
The benchmark corpus has recent_first_queue.py, generated from “write a function that returns a list of events in reverse-chronological order.” The model’s output:
def build_recent_feed(events):
feed = []
for event in events:
feed.insert(0, event)
return feed
On 10 events, this is instant. On 500,000 events — a busy system’s daily log — it’s O(N²) list rewrites. The fix:
return list(reversed(events))
Or, if you need to process events as you go: append normally, reverse at the end. BrassCoders emits PERF_LIST_INSERT_ZERO_IN_LOOP with the line number. The rule matches list.insert(0, ...) anywhere inside a loop body — framework-independent, AST-level.
The Triple-Nested Loop Join Bug
BrassCoders flags loops nested beyond a threshold as a potential O(N^K) problem. Three nested loops over three separate collections is the classic case: O(customers × orders × items) when a dict-based join would be O(customers + orders + items).
The benchmark has match_orders.py, generated from “write a function that matches customers to their orders and order items.” The model produced three nested for loops:
for customer in customers:
for order in orders:
for item in items:
if order['customer_id'] == customer['id'] and item['order_id'] == order['id']:
results.append(...)
With 1,000 customers, 5,000 orders, and 20,000 items, this is 100 billion comparisons. The fix is two dict-lookup passes — build an index from customer_id to orders, another from order_id to items, then one pass over customers.
BrassCoders emits PERF_NESTED_LOOPS_N_DEEP with the nesting depth and the innermost line. The finding doesn’t auto-rewrite the logic — that’s Claude Code’s job. BrassCoders flags it; Claude Code analyzes the join keys and proposes the indexed version.
The Unbounded while True Poll Bug
BrassCoders flags while True loops that have no size cap, no timeout binding, and no iteration limit as unbounded-resource consumers. A loop that reads from a socket, file handle, or queue with no ceiling can consume unbounded memory and run indefinitely on a hung connection or malformed input.
The benchmark has poll_until_ready.py, generated from “write a function that drains data from a socket until the connection closes.” The model’s output:
def drain_socket(sock):
data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return data
Two problems compound here: the unbounded loop and the string-concat pattern on data. A slow sender, a never-closing connection, or a large payload can pin the process indefinitely and exhaust memory. Neither Bandit nor Pylint flagged this file. BrassCoders emitted PERF_UNBOUNDED_LOOP at the while True line.
The fix adds a timeout parameter to the socket, a max_bytes cap, and replaces the string-concat with a bytearray accumulation.
Why These Four Patterns Cluster in AI-Generated Code
BrassCoders’s catch rate on these four patterns — 4 out of 4 — versus Bandit’s 0 out of 4 is not surprising once you understand what each tool is for.
Bandit is a security linter. It finds subprocess.call(shell=True), SQL string interpolation, eval() on user input, and pickle.loads(). It has no rules for algorithmic complexity because algorithmic complexity is not a security concern. Pylint finds style issues and some correctness problems. Neither tool was designed for the AI-coder performance problem.
BrassCoders added four explicit rules for these patterns because AI-generated code reliably introduces them. The prompt “write a CSV exporter” doesn’t ask for a scalable one. The model writes the obvious solution, which is the O(N²) one. The same prompt given to ten different AI coding assistants produces ten implementations with csv += in a loop. The pattern is that predictable.
Reproducing These Findings
Install BrassCoders and scan the benchmark corpus:
pip install brasscoders
git clone https://github.com/CopperSunDev/brasscoders
brasscoders --offline scan brasscoders/cli/docs/benchmarks/ai-coder-bugs/corpus/perf_antipatterns/
All four findings appear in .brass/ai_instructions.yaml, ranked by severity, with line numbers and fix-direction notes. The scan takes under a second.
Frequently Asked Questions
Why do AI coding assistants introduce these specific bugs?
AI coding assistants generate code that satisfies the prompt's stated requirements. A prompt that says 'build a CSV exporter' or 'return items in reverse order' is satisfied by code that works on small inputs. The assistant has no reason to add bounds-checking or data-structure optimization unless the prompt mentions scale — and most prompts don't. The bug is structurally invisible until the dataset grows.
Does BrassCoders catch these bugs on every project, or only specific frameworks?
BrassCoders's four perf-anti-pattern rules are language-level (Python AST), not framework-specific. The string-concatenation rule fires on any loop that grows a string with +=. The insert(0) rule fires on any loop that calls list.insert(0). The nesting rule fires above a configurable depth threshold. The unbounded-loop rule fires on while True with no detectable exit. They run on any Python codebase — Django, FastAPI, scripts, CLIs.
What's the fix for each pattern?
String concat in loop: collect into a list, then ''.join() or use io.StringIO. insert(0): reverse the append order and call list.reverse() at the end, or use collections.deque with appendleft(). Triple-nested join: build a dict keyed on the join field in one pass, then look up in O(1). Unbounded while True: add a timeout parameter, a max-iterations guard, or a size cap on accumulated data.
Do these bugs show up in code review?
Rarely. Human reviewers who see a loop exporting CSV don't mentally simulate 100,000-row inputs. AI reviewers focus on correctness and style — the code is correct for small inputs, so nothing fires. BrassCoders catches them because it applies AST-level rules on every scan regardless of input size.
Can I see the actual benchmark corpus?
Yes — the four files are at github.com/CopperSunDev/brasscoders under cli/docs/benchmarks/ai-coder-bugs/corpus/perf_antipatterns/. PROVENANCE docstrings in each file name the original prompt. Run brasscoders scan on the corpus dir to reproduce the findings.