Why browser agents fail on real websites: 7 causes
I build Dejo, a browser for AI agents, and most of what's below comes from things that broke while building it. Each cause comes with what we changed. The changes are in Dejo's changelog and its open-source code, if you want to check them.
Someone on Reddit summed up the feeling in a thread about browser setups for Claude Code: "one missed wait or stale ref breaks the whole chain." That matches what I saw. Agents rarely fail because the model is confused about the goal. They fail because the page wasn't in the state the agent assumed.
1. Acting before the content arrives
A modern dashboard often shows an empty frame first and fills in a second later. An agent that reads the page in that first second sees nothing useful, and either gives up or acts on the wrong thing.
The tempting fix is "wait 3 seconds." It's either too short (flaky on a slow day) or too long (slow on every run, forever). Dejo's tool description says it plainly: a page that loads its content late "is the most common reason a run fails."
What works is waiting for something: an element to appear, a piece of text, a URL change, or the network going quiet. In Dejo that's wait_for with a subject, like element:..., text:..., url:..., gone:... or idle. The plain wait with a number of seconds still exists, labelled as the last resort.
2. The proof shows up a moment late
This one was subtle. You click "Save", and the "Saved" message appears 800 milliseconds later. A strict check that runs immediately says the click failed, and a working step gets reported as broken.
Since 0.1.0, a step that acted gets up to 3 seconds for its check to come true before Dejo calls it broken. That's long enough for a slow confirmation, short enough that a real failure still shows up quickly.
3. Content inside an iframe
Payment forms, embedded editors and a lot of admin panels live inside iframes. Waiting for the iframe is not the same as waiting for what's in it: the frame can exist while its content is still empty.
Dejo's wait_for element: on a frame now waits for something inside it, and in_frame:<frame>|<inside> waits for one specific thing within a frame. Before that change, a step could "find" the frame and then act on an empty page.
4. Things covering the button
Cookie banners, "rate us" boxes, newsletter pop-ups. They don't show up every time, which makes them worse: a run passes on Monday and fails on Thursday because a survey appeared.
Dejo's dismiss_when_seen handles this per site: you tell it once what to click (say #accept-cookies), and from then on it clears that overlay whenever it appears, on any run. It never becomes a step in a route, so it can't break one either.
5. A dialog that asks something different
Browser confirm boxes block everything until someone answers. Playwright's own default is to dismiss them, which can quietly cancel a save: the run looks fine and nothing happened.
Dejo accepts by default, and writes down both the question and the answer. On replay, if the box that used to say "Save changes?" now says something else, the run stops instead of clicking OK. A route that learned to confirm a save should never confirm "Delete 400 rows?" just because a box appeared in the same place.
6. A selector that matches more than one thing
"Click .btn-primary" is fine until the page has three of them. Taking the first match works right up until the order changes. Playwright's locators are strict about this for the same reason: an action on a locator that matches several elements throws an error.
Dejo does the same for an agent: when a selector matches several elements, it refuses instead of guessing, and says how many it found. You can be explicit with >> nth=0 for the first or >> nth=-1 for the last, but it has to be a choice somebody made.
7. Things an agent shouldn't push through
Two failures aren't bugs at all.
A captcha is the site asking for a human. Dejo stops, says it's blocked, and doesn't mark the route as broken, because the route isn't broken. You clear the captcha once and run again.
An expired sign-in is the same kind of thing. The run reports needs_login, you sign in again, and the route carries on. Treating either of these as "the site changed" would send the agent off rewriting steps that were fine.
The pattern behind all of these
Every one of these comes down to acting on an assumption without checking it: the content is there, the click worked, the frame is ready, nothing is in the way, the dialog is the usual one, the selector means one thing. The fix each time was the same shape. Give every step a specific thing to wait for and a specific check that proves it worked, and stop with a clear reason when the check fails.
That's also what makes repeating a task cheap. Once each step can prove itself, a saved route can run again without the model watching every click. If that's the part you care about, this post has the numbers.
FAQ
Why do AI browser agents fail on real websites?
Mostly for boring reasons: they act before the page has loaded its content, a confirmation appears a moment too late, a cookie banner covers the button, content sits inside an iframe, a dialog asks something unexpected, or a selector matches more than one element. Captchas and expired sessions stop them too.
Should a browser agent wait a fixed number of seconds?
Rarely. A fixed wait is either too short, so the run is flaky, or too long, so every run is slow. It is better to wait for something specific: an element, some text, a URL, or the network going quiet.
Should an AI agent solve captchas?
No. A captcha is the site asking for a person. The agent should stop, say so, and let you clear it yourself.