Runtime Defense Tuning
Published
The bug report is always vague, because the people writing it have nothing to go on. The app breaks sometimes. Not for everyone. Not reproducibly. Often after the user has been doing something else for a while. Nobody had developer tools open, and the stack trace, if there is one at all, points into a wrapper rather than into your code. If you have debug protection switched on, there is a good chance the detector is working exactly as designed and the design has a blind spot worth understanding in detail.
What the check actually measures
The debugger-pause detector is a timing check, and its logic is short enough to describe completely. It records the wall clock, starts a repeating timer at the interval you configured, and on every tick compares the current clock against the previously recorded one. If more time has elapsed than the interval plus five hundred milliseconds, it concludes that execution was paused and runs the configured failure action. Alongside it runs a second repeating timer, at slightly under the same cadence, which invokes a debugger statement through a constructed function to add friction for anyone stepping through the code.
The premise is reasonable. A breakpoint halts the event loop, so the next scheduled callback cannot run until execution resumes, and the gap it observes will be as long as the pause. What the check does not have is any way to establish why the gap occurred. It sees lateness. It infers a debugger. Those are not the same claim, and the distance between them is where the false positives live.
The numbers, at the settings most people are running
Three values determine the behaviour, and two of them have defaults that most teams never change.
- The interval defaults to one thousand milliseconds when the setting is absent or does not parse as a number.
- The tolerance is the interval plus five hundred, so at the default the detector fires on any observed gap over fifteen hundred milliseconds.
- The value is clamped at both ends. Anything above zero but under one hundred is raised to one hundred, and anything above sixty thousand is capped at sixty thousand. Both clamps emit a build warning naming the setting, so the tool tells you when your configured value was not the value used.
- Zero disables the periodic probes while leaving the rest of the wrapper in place, and a negative value is treated as zero.
- The whole option is skipped for Node-targeted builds, with a warning explaining that it is intended for browser targets.
So the shipped default is a detector that treats one and a half seconds of lateness as evidence. Hold that number in mind for the next section, because the things that routinely exceed it are not exotic.
Everything ordinary that produces the same signal
Browsers actively work against the assumption this check relies on, and they do it for good reasons that are not going to change.
- Hidden tabs get throttled. When a page is not visible, browsers clamp repeating timers so they fire no more than about once a second. At the default interval that alone puts you at the edge of the tolerance before anything unusual has happened.
- Long-hidden tabs get throttled much harder. Chrome applies a more aggressive policy to pages that have been hidden and quiet for several minutes, running their timers roughly once a minute. That produces gaps near sixty thousand milliseconds against a tolerance of fifteen hundred, which is a fortyfold overshoot, not a near miss.
- Machines sleep. The check reads wall-clock time, so a suspend and resume shows up as a gap equal to the entire duration of the sleep. A closed laptop lid, a locked phone, a snapshotted virtual machine: all of them produce gaps measured in minutes or hours.
- The main thread blocks. A long synchronous task, a heavy layout pass, a large JSON parse or a slow extension content script can hold the event loop well past the tolerance on a loaded machine.
- Devices are slower than your laptop. An inexpensive phone with a dozen tabs open produces timer jitter that a development machine never shows you.
None of these involve a debugger, an attacker, or anything the option was built to detect. All of them are indistinguishable from one, from inside the page.
The default response is the harshest one
This is the part that turns an inaccurate detector into a broken page. When no runtime defense action is configured, the wrapper uses throw. Of the available responses, only degrade avoids ending in a thrown error; blank, reload and redirect all end that way too, and redirect additionally falls back to throwing when no destination is configured.
Put the two defaults together and the shipped behaviour is a detector with a fifteen-hundred-millisecond tolerance wired to an exception. A user who opens your application, switches to another tab for five minutes, and switches back has satisfied both halves. They did not do anything wrong, they did not open developer tools, and the page is broken when they return to it. Each default is defensible in isolation. Their combination is not, and it is the combination that ships.
Why raising the interval is necessary but not sufficient
The obvious response is to increase the interval until the reports stop, and it does help: the tolerance scales with it, so sixty seconds gives you a tolerance of sixty and a half seconds instead of one and a half. That is enough headroom to absorb ordinary jitter, slow devices and most main-thread stalls, and it is the single most effective change available.
It is not a complete fix, for two reasons. The aggressive hidden-tab policy produces gaps in the same range as the maximum you are allowed to configure, so even at the cap you are close to the line rather than clear of it. And no configurable value survives a suspended machine, because that gap is bounded by how long the user was away rather than by anything in your control. There is no interval that separates a paused debugger from a sleeping laptop, because the check measures the one property they share. Once you accept that, the interesting question stops being how to make the detector accurate and becomes what to do when it is wrong.
Tune the response, not just the threshold
The failure action is where the real decision lives, because it determines the cost of the mistakes you now know the detector will make.
- Prefer degrade or a callback. Degrade is the only response that does not end in a thrown error; a callback hands you the event and lets you decide. Either one converts a broken session into an observation.
- Send the observation somewhere. A callback that reports the trip with page visibility state and elapsed gap attached will tell you within a day whether you are detecting anything real. Most teams that instrument this discover the overwhelming majority of trips arrive with the page hidden.
- Consider zero. Setting the interval to zero switches off the periodic probes while leaving the rest of the wrapper intact. If the reports you collect show nothing but throttling, that is a well-evidenced decision rather than a retreat.
- Read the build warnings. The tool tells you when it raised your value to the minimum, capped it at the maximum, or skipped the option entirely for a Node target. A setting you believe is active and is not is a worse position than a setting you chose to disable.
- Never let it be the only thing standing between a user and a decision. Anything that matters belongs behind a server-side check, and a client-side timing heuristic is not a place to put authority.
What the option is genuinely for
None of this makes debug protection worthless; it makes its value narrower than the name suggests. Against a casual reader poking at your bundle, the repeated debugger statements are real friction, and the timing check does notice a naive stepping session. What it does not do is stop a determined analyst, who will disable the wrapper long before they are inconvenienced by it, and who is not going to be the person your false positives land on. The people who feel this option most are ordinary users with a lot of tabs open.
That asymmetry is the argument for treating it as a signal rather than a control. Turn it on if you want the friction and the telemetry, configure it so a trip is survivable, and put the things you actually care about protecting somewhere a browser is not the judge.
The short version
- The check fires when the wall clock advances more than your interval plus five hundred milliseconds between ticks.
- At the default interval of one second, that means any gap over fifteen hundred milliseconds.
- Hidden tabs are throttled to about once a second, and long-hidden tabs in Chrome to about once a minute.
- A suspended machine produces a gap equal to the length of the sleep, which no setting can absorb.
- The default response is throw, and only degrade avoids ending in a thrown error.
- Raise the interval, switch the response to degrade or a callback, and report what you catch before deciding whether to keep it.
Frequently asked questions
How does the debugger-pause check actually detect anything?
By measuring elapsed wall-clock time between ticks of a repeating timer. The wrapper records the clock, schedules an interval at whatever you set for DebugProtectionIntervalMilliseconds, and on each tick compares the current clock against the previous one. If the gap exceeds the interval plus five hundred milliseconds, it treats that as a pause and runs the failure action. The reasoning is sound as far as it goes: a breakpoint stops the event loop, so the next tick arrives late. The problem is that the detector observes lateness, not causation, and a great many ordinary things make a timer late.
Why does a backgrounded tab trip it?
Because browsers deliberately slow timers down in pages the user is not looking at, and the detector has no way to tell that apart from a debugger. Hidden pages generally have repeating timers clamped to no more often than once a second, and Chrome applies a more aggressive policy to pages that have been hidden and quiet for several minutes, dropping them to roughly once a minute to save battery. At the default interval of one second the detector's tolerance is fifteen hundred milliseconds. A tab under the aggressive policy produces gaps around sixty thousand. That is not a marginal miss, and it means the check is firing on ordinary tab-switching behaviour rather than on anybody's debugger.
Does closing a laptop lid trip it too?
Yes, and this one is harder to argue with because the gap is enormous. The check reads the wall clock rather than a monotonic performance counter, so when a machine suspends and later resumes, the elapsed time it sees is the whole duration of the sleep. A laptop closed over lunch produces a gap of hours against a tolerance measured in hundreds of milliseconds. Anything else that suspends a process has the same effect: a virtual machine snapshot, an operating system putting a background process to sleep, a phone locking. The user experiences it as the page being broken when they come back to it.
What happens when it fires, by default?
It throws. When no runtime defense action is configured the wrapper uses throw, and of the available responses only degrade avoids ending in a thrown error. So the default combination is a detector with a fifteen-hundred-millisecond tolerance wired to the harshest available response. A user who switches away from your tab for five minutes and comes back has, at that point, met both conditions. This is the single most important thing to know about the option, because the two defaults are individually reasonable and jointly produce the outcome nobody wanted.
Should we just raise the interval until it stops firing?
Raising it helps and it does not solve the problem, so do it as one change among several. The tolerance is the interval plus five hundred milliseconds, so a larger interval buys proportionally more headroom, and the value is clamped to a maximum of sixty seconds with a build warning telling you it was capped. But an aggressively throttled tab is producing gaps in the same range as that maximum, and a suspended machine is producing gaps far beyond anything you can configure. No interval separates a paused debugger from a sleeping laptop, because the detector is measuring the one thing they have in common. Change the response as well as the interval.
What is a sensible configuration if we want to keep the option on?
Treat it as telemetry rather than as enforcement. Raise the interval substantially so ordinary lateness stays inside the tolerance. Set the response to degrade or to a callback of your own, so a trip reports the event instead of ending the session, and send that report somewhere you will actually look. If the numbers you collect show the check firing on real users at a rate you cannot explain, that is your answer about whether to keep it. Set the interval to zero to switch off the periodic probes while keeping the rest of the wrapper, and remember it is skipped altogether for Node-targeted builds with a warning saying so.
Related reading