Where CDP fits
Every library that controls a Chromium browser ultimately speaks CDP under the hood. Puppeteer and Playwright wrap it in friendlier APIs and add their own conveniences, such as auto-waiting (pausing until an element is ready) and selector engines (helpers for finding elements on the page). For about 95% of scraping you want the wrapper, not raw CDP. The main exception is when you need to attach to a real user's Chrome - a profile that already has cookies, history, and extensions installed - instead of launching a fresh headless instance (a browser with no visible window). In that case, talking to CDP directly through its WebSocket endpoint (the live two-way connection the browser opens for debugging) is the cleanest path.
Detection considerations
Chrome only opens the CDP port when you launch it with the --remote-debugging-port flag. Some defensive scripts probe for this and flag the session - but it is a weak signal, because the port is visible only to the host machine, not to the page itself. The stronger CDP-related giveaway is the Runtime.enable domain being active in the page context, which Puppeteer and Playwright switch on by default. (A domain is one feature area of CDP; Runtime.enable turns on JavaScript-execution hooks, and turning it on leaves traces a page can notice.) Some automation tools toggle these domains off when they are not needed.
When to use CDP directly
Three real cases call for raw CDP: (1) attaching to an existing Chrome process that uses a real profile, (2) building custom request interception that Playwright's API does not expose, and (3) building a tool that needs precise control over which CDP domains are enabled. For everything else, Playwright or Puppeteer is the better default.
