Chat-based AI comparisons — paste a prompt into ChatGPT, Claude, or Gemini and compare the reply — miss what actually matters about in-editor AI assistants: whether they understand your existing codebase well enough to make a coherent multi-file change without you copy-pasting context by hand. I ran the same real task through GitHub Copilot, Cursor, and Windsurf on the same small Laravel codebase and kept the actual diffs, not just impressions.
The task, identical across all three
Add rate limiting to an existing URL shortener's redirect endpoint — the same kind of feature backing a tool like this site's own URL shortener — specifically: limit redirect lookups to 60 per minute per IP, return a proper 429 response with a Retry-After header, and add a test covering the limit being hit. This touches at least three files (the route, a middleware or controller change, and a test), which is exactly the kind of multi-file coherence chat-based comparisons don't exercise.
GitHub Copilot: fast, but I drove every file change myself
Copilot's inline suggestions were genuinely fast and mostly correct at the line level — accepting its suggestion for the middleware's handle() method needed almost no editing. But Copilot Chat's workspace-edit feature, at the time I tested it, proposed changes to the middleware and the test file as two separate suggestions I had to review and apply individually, rather than a single coherent multi-file change I could accept once. It never touched the route file's registration of the middleware at all — I had to notice that gap and wire it in myself.
// Copilot's middleware suggestion — good on its own, needed manual wiring
class RedirectRateLimiter
{
public function handle(Request $request, Closure $next)
{
$key = 'redirect-limit:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 60)) {
return response()->json(['message' => 'Too many requests.'], 429)
->header('Retry-After', RateLimiter::availableIn($key));
}
RateLimiter::hit($key, 60);
return $next($request);
}
}
The code itself was fine. What Copilot didn't do was treat this as one connected change — I was still the one holding the mental model of "route, middleware, test" together across three separate accept-and-review steps.
Cursor: genuinely multi-file, one real mistake in the diff
Cursor's Composer feature took the same prompt and proposed a single diff spanning all three files at once — the middleware, the route registration, and a Pest test — which is the actual thing chat-based tools and Copilot's suggestion-by-suggestion flow can't do. The diff was reviewable as one unit before I accepted anything, which meant I could evaluate the whole change's coherence rather than three disconnected pieces.
The mistake: the generated test asserted a 429 status code after exactly 60 requests, but the loop in the test only sent 59 requests before the assertion — an off-by-one that would have passed for the wrong reason (the 60th request, which should trigger the limit, was never actually sent in the test).
// Cursor's test — off by one, caught on review
for ($i = 0; $i < 59; $i++) { // should be <= 60 or $i < 60 with one more assertion
$this->get('/s/abc123')->assertOk();
}
$this->get('/s/abc123')->assertStatus(429); // this WAS request #60, so it passed by luck
It technically passed, which is the more dangerous version of this bug — an off-by-one that happens to still produce a green test is much easier to ship unnoticed than one that fails outright. Catching it required actually reading the loop bounds against the stated limit rather than trusting a passing test, the same discipline covered in where AI tools quietly get things wrong specifically for counting and boundary errors.
Windsurf: the most autonomous, and the one that needed the most correction
Windsurf's Cascade agent went further than either of the others — it not only wrote the middleware, route change, and test, but also proactively added a config value for the rate limit threshold and updated a README section documenting the endpoint's behavior, unprompted. That extra initiative was a genuine net positive for this task. The correction needed was in the middleware itself: it keyed the rate limit purely by IP with no allowance for the possibility that $request->ip() returns a shared proxy address in our actual deployment (we sit behind a load balancer), which — unaddressed — would have rate-limited our entire user base as if it were one client the moment it shipped.
// Windsurf's version — didn't account for our load balancer setup
$key = 'redirect-limit:' . $request->ip();
// What it needed to be, given our actual infrastructure:
$key = 'redirect-limit:' . $request->header('X-Forwarded-For', $request->ip());
This wasn't a bug in the abstract — the code was reasonable for a typical deployment. It was wrong specifically for our infrastructure, and no AI assistant working from the code alone could have known about our load balancer configuration without that context being available to it somewhere in the codebase or being told directly.
Side-by-side summary
| Tool | Multi-file coherence | What needed a manual fix | Best fit |
|---|---|---|---|
| Copilot | Suggestion-by-suggestion, not unified | Missing route wiring, no test link | Fast line-level completions inside a file you're already editing |
| Cursor | Single reviewable multi-file diff | Off-by-one in generated test loop | Reviewing one coherent change across a small number of files |
| Windsurf | Most autonomous, added extras unprompted | Missed infrastructure-specific detail (load balancer) | Larger, more self-directed changes where extra initiative is welcome |
A second task: fixing a bug across an existing pattern
To test something other than adding new code, I gave all three a second task on the same codebase: an existing validation rule allowed an empty string to pass a "required" check because of a loose comparison, and the same buggy pattern was copy-pasted across four different form request classes. The task: find every instance and fix the underlying comparison consistently.
- Copilot found and fixed the instance in the file I had open, but its chat search didn't proactively surface the other three copies elsewhere in the codebase — I had to grep for the pattern myself and revisit each file individually.
- Cursor, using its codebase-wide search, found all four instances in one pass and proposed a single diff fixing all of them consistently, which is exactly the kind of task its multi-file coherence is built for.
- Windsurf also found all four, and additionally flagged a fifth, related-but-not-identical validation pattern nearby that had a similar but not exact issue — a genuinely useful catch, though it required me to actually read its explanation of why the fifth one was different rather than blindly accepting a fifth fix that wasn't quite the same bug.
This second task separated the three more clearly than the first one did — codebase-wide pattern-matching across files is exactly where a tool's search and context-gathering approach matters most, and it's a genuinely different skill from generating correct code for a single, well-scoped new feature.
What this task didn't test
A single feature addition on a small, well-organized codebase is a favorable case for all three tools — none of them had to navigate genuine ambiguity about where code should live, or reconcile with a messy or inconsistent existing pattern beyond the second task's deliberately narrow scope. On a larger, more tangled codebase I'd expect the gap between "suggestion-by-suggestion" and "coherent multi-file diff" tools to widen further, since the cost of manually holding several related changes in your head scales with how much context those changes actually depend on. I also didn't test any of the three on a genuinely large monorepo, where indexing time and search quality across tens of thousands of files is its own separate axis these two small tasks don't exercise at all.
Cost, since it's part of the real decision
None of the three are free at the usage level a working developer needs, and the pricing structures aren't directly comparable line for line — Copilot's individual plan is billed flat monthly regardless of usage, while Cursor and Windsurf's higher tiers are built around a pool of premium model requests per month with overage pricing once you exceed it. For light, occasional use, that difference barely matters. For someone running Composer- or Cascade-style multi-file agent tasks daily, the request-pool model can get expensive faster than a flat per-seat price if usage isn't monitored, which is worth checking against your own actual usage pattern before assuming the flat-rate option is automatically cheaper — for a team running a lot of small, quick completions rather than large agentic edits, it often is, but that's a usage-shape question, not a universal rule.
What I'd actually recommend based on this
If your daily work is mostly small, in-file completions with occasional single-file chat questions, Copilot's simplicity and predictable flat pricing is a genuinely reasonable default — you're not paying for multi-file agent capability you won't use often. If you're regularly making changes that span several related files and want to review one coherent diff rather than piece together several separate suggestions, Cursor's Composer is worth the switch. If you want a tool that takes more initiative on larger, more loosely-specified tasks and you're comfortable reviewing more of its output carefully as a trade-off for that initiative, Windsurf's Cascade is worth trying — with the explicit caveat, demonstrated directly by the load-balancer example above, that more autonomy also means more surface area for it to get something environment-specific wrong without you noticing until you look closely.
The actual takeaway
None of these three caught everything, and none of them produced code I'd ship without reading it — which is the whole point of testing on a real task instead of trusting a demo. What differed wasn't raw code quality so much as how much of the "hold the whole change together correctly" work each tool did versus left to me, and that's a genuinely different axis of comparison than the chat-based model comparisons cover, which is why the ChatGPT vs Claude vs Gemini comparison and this one aren't really answering the same question, even though they sound similar on the surface. If your team is also weighing which editor to build a workflow around in the first place, that decision compounds with this one — see VS Code vs JetBrains for the editor-level side of that same tooling decision.
No comments yet.
Be the first visitor to add a thoughtful comment on this article.