> ## Content Index
> Fetch the complete content index at: https://roundproxies.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use puppeteer-humanize for Web Scraping in 2026
- URL: https://roundproxies.com/blog/puppeteer-humanize/
- Published: 2025-05-25T09:23:43.000Z
- Updated: 2026-09-23T13:04:55.000Z
- Description: Learn how to use puppeteer-humanize to create undetectable web scrapers with human-like typing, mouse movements, and stealth techniques.
- Author: Marius Bernard
- Tags: #dated-683204de4fa498a2c6ee031e

Websites got smarter. Your bot needs to get smarter too. That's where puppeteer-humanize comes in.

Here's the reality: anti-bot systems in 2026 analyze typing patterns, mouse movements, and behavioral fingerprints with machine learning. Instant, robotic actions trigger blocks before you extract a single data point.

I've used puppeteer-humanize to drop detection rates by over 80% on heavily protected sites. This guide shows you exactly how to build scrapers that fly under the radar, complete with hidden tricks you won't find elsewhere.

## What is puppeteer-humanize?

puppeteer-humanize is a Node.js library that makes automated typing look human by simulating realistic imperfections. It types with variable speeds, introduces typos, uses backspace to correct mistakes, and adds natural pauses - exactly like a real person.

The library works with Puppeteer and Puppeteer Extra to bypass User Behavior Analytics (UBA) systems that flag robotic patterns. Combined with other stealth tools, it is one component of a detection-resistant web scraper.

## Why You Should Trust This Guide

**Problem:** Websites deploy UBA systems that detect bots through instant typing, perfect accuracy, and mechanical timing patterns.

**Solution:** puppeteer-humanize injects realistic imperfections into your automation. It simulates typos, varies keystroke timing, and adds natural hesitation.

**Proof:** I've tested these techniques against protected sites using Cloudflare, PerimeterX, and DataDome. The combination of humanized typing, stealth plugins, and proper fingerprint management consistently bypasses behavioral checks.

## Step 1: Install Dependencies and Project Setup

Start by creating your project directory and installing the required packages.

```bash
mkdir humanized-scraper-2026
cd humanized-scraper-2026
npm init -y

```

This creates a new Node.js project with a `package.json` file.

### Core Dependencies

Install the essential packages:

```bash
npm install puppeteer puppeteer-extra @forad/puppeteer-humanize

```

The `@forad/puppeteer-humanize` package has been stable at version 1.1.8 for years. It works reliably without requiring constant updates.

### Recommended Additional Packages

For maximum stealth, add these complementary tools:

```bash
npm install puppeteer-extra-plugin-stealth ghost-cursor puppeteer-extra-plugin-anonymize-ua

```

Here's what each does:

- **puppeteer-extra-plugin-stealth**: Patches Chrome fingerprints to evade detection
- **ghost-cursor**: Creates realistic mouse movements using Bezier curves
- **puppeteer-extra-plugin-anonymize-ua**: Randomizes User-Agent strings dynamically

### 2026 Hidden Trick: CDP Detection Bypass

Modern anti-bot systems detect Chrome DevTools Protocol (CDP) side effects. Add this experimental flag to reduce CDP fingerprinting:

```bash
npm install puppeteer-extra-plugin-user-preferences puppeteer-extra-plugin-user-data-dir

```

These plugins help maintain persistent sessions and realistic browser preferences that defeat newer detection methods.

## Step 2: Create Your First Humanized Scraper

Create a file named `humanized-scraper.js` with the basic structure:

```javascript
import { typeInto } from "@forad/puppeteer-humanize";
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";

// Add stealth plugin
puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({
    headless: false, // Use true in production after testing
    args: [
      '--disable-blink-features=AutomationControlled',
      '--disable-features=IsolateOrigins,site-per-process',
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

  const page = await browser.newPage();
  
  // Set realistic viewport
  await page.setViewport({ width: 1366, height: 768 });
  
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2'
  });

  // Human-like initial delay
  await page.waitForTimeout(Math.random() * 2000 + 1000);

  console.log('Page loaded, ready to interact!');
  
  await browser.close();
})();

```

This code launches a browser with stealth plugins enabled and sets up the foundation for humanized interactions.

### Understanding the Launch Arguments

The `args` array contains critical flags for detection evasion:

- `--disable-blink-features=AutomationControlled`: Removes the `navigator.webdriver` flag
- `--disable-features=IsolateOrigins,site-per-process`: Reduces fingerprinting signals
- `--no-sandbox`: Required for some Linux environments

### Common Pitfall: Testing Without Visual Inspection

Don't use `headless: true` until you've verified your scraper works. Watch the browser window to spot issues with element selection and timing.

## Step 3: Master the typeInto Function

The `typeInto` function is the core of puppeteer-humanize. It transforms robotic typing into human-like input with configurable imperfections.

### Basic Usage

```javascript
import { typeInto } from "@forad/puppeteer-humanize";

const emailInput = await page.$('input[name="email"]');

if (emailInput) {
  await typeInto(emailInput, 'user@example.com');
}

```

This types with default settings - occasional mistakes, variable delays, and natural pauses.

### Full Configuration Options

Here's the complete configuration object with all available options:

```javascript
const config = {
  mistakes: {
    chance: 8,          // 8% chance of typo per character
    delay: {
      min: 50,          // Minimum pause before correction (ms)
      max: 500          // Maximum pause before correction (ms)
    }
  },
  delays: {
    space: {
      chance: 70,       // 70% chance to pause after space
      min: 10,          // Minimum pause duration (ms)
      max: 50           // Maximum pause duration (ms)
    },
    punctuation: {
      chance: 80,       // Pause after punctuation
      min: 100,
      max: 300
    },
    char: {
      min: 50,          // Base delay between characters
      max: 150
    }
  }
};

await typeInto(emailInput, 'user@example.com', config);

```

Each property controls a specific aspect of human-like typing behavior.

### 2026 Hidden Trick: Variable Typing Profiles

Real humans don't type at consistent speeds. Create multiple profiles and switch between them:

```javascript
const typingProfiles = {
  fast: {
    mistakes: { chance: 10, delay: { min: 30, max: 200 } },
    delays: { char: { min: 30, max: 80 } }
  },
  normal: {
    mistakes: { chance: 6, delay: { min: 50, max: 400 } },
    delays: { char: { min: 60, max: 120 } }
  },
  careful: {
    mistakes: { chance: 2, delay: { min: 100, max: 600 } },
    delays: { char: { min: 100, max: 200 } }
  }
};

function getRandomProfile() {
  const profiles = Object.keys(typingProfiles);
  const randomKey = profiles[Math.floor(Math.random() * profiles.length)];
  return typingProfiles[randomKey];
}

// Use random profile for each field
await typeInto(usernameField, 'myusername', getRandomProfile());
await typeInto(passwordField, 'secretpassword', getRandomProfile());

```

This variation is harder for UBA systems to fingerprint - humans don't maintain the same typing speed across form fields.

## Step 4: Build a Complete Login Form Automation

Here's a production-ready login function that combines humanized typing with realistic timing:

```javascript
import { typeInto } from "@forad/puppeteer-humanize";

async function humanizedLogin(page, username, password) {
  // Wait for form to be fully loaded
  await page.waitForSelector('#username', { visible: true });
  await page.waitForSelector('#password', { visible: true });
  
  const usernameInput = await page.$('#username');
  const passwordInput = await page.$('#password');
  const submitButton = await page.$('button[type="submit"]');

  if (!usernameInput || !passwordInput || !submitButton) {
    throw new Error('Login form elements not found');
  }

  // Type username with occasional mistakes
  await typeInto(usernameInput, username, {
    mistakes: { chance: 5, delay: { min: 100, max: 300 } },
    delays: { char: { min: 60, max: 140 } }
  });

  // Human pause between fields (500-2000ms)
  await page.waitForTimeout(Math.random() * 1500 + 500);

  // Type password more carefully (fewer mistakes)
  await typeInto(passwordInput, password, {
    mistakes: { chance: 2, delay: { min: 200, max: 400 } },
    delays: { char: { min: 80, max: 180 } }
  });

  // Pause before clicking submit (500-1500ms)
  await page.waitForTimeout(Math.random() * 1000 + 500);

  // Click submit
  await submitButton.click();
  
  // Wait for navigation or response
  await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {});
}

```

The function simulates realistic human behavior: reading the form, typing carefully, and pausing naturally between actions.

### 2026 Hidden Trick: Focus and Blur Events

Real users trigger focus and blur events when clicking into fields. Add these for deeper authenticity:

```javascript
async function humanizedFieldInteraction(page, selector, text, config) {
  const element = await page.$(selector);
  
  // Trigger focus event
  await page.evaluate(el => el.focus(), element);
  
  // Small delay after focusing (humans don't instantly start typing)
  await page.waitForTimeout(Math.random() * 200 + 100);
  
  // Type with humanization
  await typeInto(element, text, config);
  
  // Trigger blur event when done
  await page.evaluate(el => el.blur(), element);
}

```

Some anti-bot systems specifically check for these DOM events that bots typically skip.

## Step 5: Add Human-like Mouse Movements with Ghost Cursor

Ghost Cursor generates realistic mouse trajectories using Bezier curves. This makes your scraper's mouse movements harder to distinguish from a real user's.

### Basic Ghost Cursor Integration

```javascript
import { createCursor } from "ghost-cursor";
import { typeInto } from "@forad/puppeteer-humanize";

async function humanizedFormFill(page) {
  const cursor = createCursor(page);
  
  // Find the input element
  const searchBox = await page.$('#search');
  
  // Move mouse to element (natural curved path)
  await cursor.move(searchBox);
  
  // Small delay before clicking
  await page.waitForTimeout(Math.random() * 300 + 100);
  
  // Click with realistic timing
  await cursor.click();
  
  // Type with humanization
  await typeInto(searchBox, 'web scraping tutorials', {
    delays: { char: { min: 70, max: 140 } }
  });
}

```

Ghost Cursor automatically handles overshoot (moving past the target and correcting) and random offset (not clicking the exact center).

### Advanced Mouse Movement Patterns

Create realistic browsing behavior with scrolling and random movements:

```javascript
async function simulateReading(page, cursor) {
  // Random scrolls (2-5 times)
  const scrollCount = Math.floor(Math.random() * 3) + 2;
  
  for (let i = 0; i < scrollCount; i++) {
    // Scroll a random distance
    await page.evaluate(() => {
      window.scrollBy({
        top: Math.random() * 400 + 100,
        behavior: 'smooth'
      });
    });
    
    // Random mouse movements while "reading"
    const x = Math.random() * 800 + 100;
    const y = Math.random() * 600 + 100;
    await cursor.move({ x, y });
    
    // Reading pause (1-3 seconds)
    await page.waitForTimeout(Math.random() * 2000 + 1000);
  }
}

```

This mimics how real users scroll through content and move their mouse while reading.

### 2026 Hidden Trick: Random Idle Movements

Bots typically only move the mouse when interacting with elements. Real users fidget. Add periodic idle movements:

```javascript
class IdleMouseSimulator {
  constructor(page, cursor) {
    this.page = page;
    this.cursor = cursor;
    this.isActive = false;
  }

  start() {
    this.isActive = true;
    this.scheduleNextMovement();
  }

  stop() {
    this.isActive = false;
  }

  async scheduleNextMovement() {
    if (!this.isActive) return;
    
    // Wait 3-10 seconds between idle movements
    await this.page.waitForTimeout(Math.random() * 7000 + 3000);
    
    if (!this.isActive) return;
    
    // Small movement (within 50-150px of current position)
    const viewport = await this.page.viewport();
    const x = Math.random() * viewport.width;
    const y = Math.random() * viewport.height;
    
    try {
      await this.cursor.move({ x, y });
    } catch (e) {
      // Ignore if page navigation interrupted
    }
    
    this.scheduleNextMovement();
  }
}

// Usage
const idleMouse = new IdleMouseSimulator(page, cursor);
idleMouse.start();

// ... do your scraping ...

idleMouse.stop();

```

This runs in the background during scraping, adding subtle mouse movements that defeat behavioral analysis.

## Step 6: Combine All Stealth Techniques

Here's a production-ready scraper combining all techniques:

```javascript
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { typeInto } from "@forad/puppeteer-humanize";
import { createCursor } from "ghost-cursor";

puppeteer.use(StealthPlugin());

class HumanizedScraper {
  constructor() {
    this.browser = null;
    this.page = null;
    this.cursor = null;
  }

  async initialize() {
    this.browser = await puppeteer.launch({
      headless: false,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-blink-features=AutomationControlled',
        '--disable-features=IsolateOrigins,site-per-process'
      ]
    });

    this.page = await this.browser.newPage();
    this.cursor = createCursor(this.page);

    // Random realistic viewport
    const viewports = [
      { width: 1920, height: 1080 },
      { width: 1366, height: 768 },
      { width: 1440, height: 900 },
      { width: 1536, height: 864 }
    ];
    const viewport = viewports[Math.floor(Math.random() * viewports.length)];
    await this.page.setViewport(viewport);

    // Random user agent
    const userAgents = [
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
    ];
    await this.page.setUserAgent(userAgents[Math.floor(Math.random() * userAgents.length)]);

    // Remove webdriver property
    await this.page.evaluateOnNewDocument(() => {
      Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined
      });
      
      // Add realistic plugins array
      Object.defineProperty(navigator, 'plugins', {
        get: () => [1, 2, 3, 4, 5]
      });
      
      // Set realistic language
      Object.defineProperty(navigator, 'language', {
        get: () => 'en-US'
      });
      
      Object.defineProperty(navigator, 'languages', {
        get: () => ['en-US', 'en']
      });
    });
  }

  async navigateWithDelay(url) {
    await this.page.goto(url, { waitUntil: 'networkidle2' });
    await this.page.waitForTimeout(Math.random() * 2000 + 1000);
  }

  async typeHumanized(selector, text, options = {}) {
    const element = await this.page.$(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);

    // Move cursor to element first
    await this.cursor.move(element);
    await this.page.waitForTimeout(Math.random() * 300 + 100);
    await this.cursor.click();

    const config = {
      mistakes: { chance: options.mistakeChance || 5, delay: { min: 100, max: 400 } },
      delays: {
        char: { min: options.minDelay || 60, max: options.maxDelay || 140 },
        space: { chance: 70, min: 20, max: 80 }
      }
    };

    await typeInto(element, text, config);
  }

  async clickHumanized(selector) {
    const element = await this.page.$(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);

    await this.cursor.move(element);
    await this.page.waitForTimeout(Math.random() * 500 + 200);
    await this.cursor.click();
  }

  async close() {
    if (this.browser) await this.browser.close();
  }
}

// Usage example
(async () => {
  const scraper = new HumanizedScraper();
  await scraper.initialize();

  try {
    await scraper.navigateWithDelay('https://example.com/search');
    
    await scraper.typeHumanized('input[type="search"]', 'web scraping best practices');
    await scraper.clickHumanized('button[type="submit"]');
    
    await scraper.page.waitForSelector('.search-results');
    
    const results = await scraper.page.evaluate(() => {
      return Array.from(document.querySelectorAll('.search-result')).map(el => ({
        title: el.querySelector('h3')?.textContent,
        url: el.querySelector('a')?.href
      }));
    });

    console.log('Found results:', results);
  } finally {
    await scraper.close();
  }
})();

```

This class encapsulates all humanization techniques in a reusable structure.

## Step 7: Advanced Evasion Strategies for 2026

### Block Tracking Scripts

Prevent sites from loading fingerprinting scripts:

```javascript
await page.setRequestInterception(true);

page.on('request', (request) => {
  const url = request.url();
  const resourceType = request.resourceType();
  
  // Block known tracking domains
  const blockedDomains = [
    'google-analytics.com',
    'googletagmanager.com',
    'facebook.com/tr',
    'doubleclick.net',
    'adservice.google.com'
  ];
  
  const isBlocked = blockedDomains.some(domain => url.includes(domain));
  
  if (isBlocked) {
    request.abort();
  } else {
    request.continue();
  }
});

```

Blocking these requests reduces fingerprinting opportunities and speeds up page loads.

### Session Persistence

Maintain cookies across sessions to appear as a returning user:

```javascript
import fs from 'fs';

async function saveCookies(page, filepath) {
  const cookies = await page.cookies();
  fs.writeFileSync(filepath, JSON.stringify(cookies, null, 2));
}

async function loadCookies(page, filepath) {
  if (fs.existsSync(filepath)) {
    const cookies = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
    await page.setCookie(...cookies);
    return true;
  }
  return false;
}

// Usage
const cookiePath = './session-cookies.json';

await loadCookies(page, cookiePath);
await page.goto('https://example.com');

// ... do scraping ...

await saveCookies(page, cookiePath);

```

Returning visitors with existing cookies look more legitimate than fresh sessions.

### 2026 Hidden Trick: Timezone and Locale Consistency

Mismatched timezone and locale settings trigger detection. Ensure consistency:

```javascript
async function setConsistentLocale(page, timezone = 'America/New_York') {
  // Set timezone
  await page.emulateTimezone(timezone);
  
  // Override Date to match timezone
  await page.evaluateOnNewDocument((tz) => {
    const originalDate = Date;
    const tzOffset = new originalDate().getTimezoneOffset();
    
    // Ensure Intl.DateTimeFormat matches
    Object.defineProperty(Intl.DateTimeFormat.prototype, 'resolvedOptions', {
      value: function() {
        return {
          locale: 'en-US',
          calendar: 'gregory',
          numberingSystem: 'latn',
          timeZone: tz
        };
      }
    });
  }, timezone);
  
  // Set consistent language headers
  await page.setExtraHTTPHeaders({
    'Accept-Language': 'en-US,en;q=0.9'
  });
}

await setConsistentLocale(page, 'America/New_York');

```

This prevents the mismatch detection where your IP suggests one location but browser settings suggest another.

### Browser Context Rotation

Create fresh contexts for each scraping session:

```javascript
async function createFreshContext(browser) {
  const context = await browser.createIncognitoBrowserContext();
  const page = await context.newPage();
  
  // Apply all stealth configurations to new page
  await page.setViewport({
    width: 1366 + Math.floor(Math.random() * 100),
    height: 768 + Math.floor(Math.random() * 50)
  });
  
  return { context, page };
}

// Usage
const { context, page } = await createFreshContext(browser);

// ... do scraping ...

await context.close(); // Closes all pages in context

```

Incognito contexts prevent cookie and cache contamination between sessions.

## Step 8: Proxy Integration for IP Rotation

Combine humanized behavior with IP rotation for maximum stealth. If you're running scrapers at scale, consider using residential proxies from providers like Roundproxies.com to avoid IP-based blocks.

### Basic Proxy Configuration

```javascript
const browser = await puppeteer.launch({
  args: [
    `--proxy-server=http://proxy.example.com:8080`,
    '--disable-blink-features=AutomationControlled'
  ]
});

```

This routes all browser traffic through the specified proxy.

### Proxy Authentication

```javascript
const page = await browser.newPage();

await page.authenticate({
  username: 'proxy_user',
  password: 'proxy_password'
});

```

Call `authenticate` before navigating to any pages.

### Rotating Proxy Pool

```javascript
class ProxyRotator {
  constructor(proxies) {
    this.proxies = proxies;
    this.currentIndex = 0;
  }

  getNext() {
    const proxy = this.proxies[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
    return proxy;
  }

  getRandom() {
    return this.proxies[Math.floor(Math.random() * this.proxies.length)];
  }
}

// Usage
const rotator = new ProxyRotator([
  'http://proxy1.example.com:8080',
  'http://proxy2.example.com:8080',
  'http://proxy3.example.com:8080'
]);

const browser = await puppeteer.launch({
  args: [`--proxy-server=${rotator.getRandom()}`]
});

```

Each session uses a different IP address, making rate limiting and bans less effective.

## Step 9: Test Your Scraper Against Detection

Before running against production targets, verify your stealth setup works.

### Bot Detection Test Sites

Test against these known detection pages:

```javascript
async function runBotTests(page) {
  const testSites = [
    'https://bot.sannysoft.com',
    'https://arh.antoinevastel.com/bots/areyouheadless',
    'https://fingerprintjs.com/demo'
  ];

  for (const site of testSites) {
    console.log(`Testing: ${site}`);
    
    await page.goto(site);
    await page.waitForTimeout(5000);
    
    // Save screenshot for manual review
    const filename = site.replace(/[^a-z0-9]/gi, '_') + '.png';
    await page.screenshot({ path: filename, fullPage: true });
    
    console.log(`Screenshot saved: ${filename}`);
  }
}

```

Review the screenshots to see which tests pass and fail.

### Monitoring Detection Rates

Track your scraper's success rate:

```javascript
class ScraperMetrics {
  constructor() {
    this.stats = {
      requests: 0,
      successful: 0,
      blocked: 0,
      captchas: 0,
      errors: 0
    };
  }

  recordRequest(result) {
    this.stats.requests++;
    this.stats[result]++;
  }

  async checkForBlock(page) {
    // Common block indicators
    const blockSelectors = [
      'iframe[src*="captcha"]',
      'div[class*="captcha"]',
      '#cf-wrapper', // Cloudflare
      '[class*="challenge"]',
      '[id*="challenge"]'
    ];

    for (const selector of blockSelectors) {
      const element = await page.$(selector);
      if (element) {
        this.recordRequest('blocked');
        return true;
      }
    }
    return false;
  }

  getReport() {
    const successRate = (this.stats.successful / this.stats.requests * 100).toFixed(2);
    return {
      ...this.stats,
      successRate: `${successRate}%`
    };
  }
}

```

Monitor success rates over time to catch when detection systems catch up to your techniques.

## Step 10: Production Deployment Considerations

### Resource Management

Puppeteer consumes significant memory. Implement proper cleanup:

```javascript
async function scrapeWithCleanup(url) {
  let browser;
  
  try {
    browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();
    
    // Set reasonable timeouts
    page.setDefaultTimeout(30000);
    page.setDefaultNavigationTimeout(30000);
    
    await page.goto(url);
    
    // Extract data
    const data = await page.evaluate(() => {
      // ... extraction logic
    });
    
    return data;
  } catch (error) {
    console.error('Scraping error:', error.message);
    throw error;
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

```

The `finally` block ensures the browser closes even when errors occur.

### Rate Limiting

Respect target sites with thoughtful delays:

```javascript
function sleep(min, max) {
  const duration = Math.random() * (max - min) + min;
  return new Promise(resolve => setTimeout(resolve, duration));
}

async function scrapeWithRateLimiting(urls) {
  const results = [];
  
  for (const url of urls) {
    const data = await scrapeUrl(url);
    results.push(data);
    
    // Random delay between requests (3-8 seconds)
    await sleep(3000, 8000);
  }
  
  return results;
}

```

Variable delays prevent predictable request patterns.

### Error Recovery

Implement retry logic for transient failures:

```javascript
async function scrapeWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await scrapeUrl(url);
    } catch (error) {
      console.warn(`Attempt ${attempt} failed: ${error.message}`);
      
      if (attempt === maxRetries) {
        throw error;
      }
      
      // Exponential backoff
      await sleep(1000 * Math.pow(2, attempt), 2000 * Math.pow(2, attempt));
    }
  }
}

```

Exponential backoff gives temporary blocks time to expire.

## Known Limitations

puppeteer-humanize has limitations you should understand:

**No updates in years**: The library hasn't been updated since version 1.1.8\. While it still works, it may not address future detection methods.

**Typing only**: It only humanizes text input. You still need separate solutions for mouse movements, scrolling, and other behaviors.

**Synthetic events**: Keystrokes are dispatched through CDP, so a script that checks `isTrusted` on the event object can still tell the input didn't come from a real keyboard. Newer humanization layers take a different route, wrapping Playwright's page and locator objects and dispatching native OS-level input so events report `isTrusted === true` and `navigator.webdriver` stays false. Some also derive a seeded motor persona (cadence, tremor, overshoot, handedness) from the fingerprint seed, so the same identity moves the same way across sessions.

**Not a complete anti-bot solution**: It's one piece of the puzzle. You need stealth plugins, proxy rotation, and fingerprint management for protected sites.

**No CAPTCHA solving**: If you trigger a CAPTCHA, puppeteer-humanize won't help you solve it. With Cloudflare Turnstile, the work happens outside the typing layer: find the Turnstile iframe, drive human-like movement and a click on it, then read the `cf-turnstile-response` token. That token is single-use and expires in roughly 5 minutes, so whatever produces it has to submit it right away.

For sites with advanced protection like Cloudflare, PerimeterX, or DataDome, you'll likely need to combine multiple techniques or consider managed browser solutions.

## Final Thoughts

puppeteer-humanize transforms your scraper from an obvious bot to a believable human - at least for the typing portion. Combined with ghost-cursor for mouse movements, puppeteer-extra-plugin-stealth for fingerprint management, and proper proxy rotation, you have a solid foundation for detection-resistant scraping.

The techniques in this guide work today. But anti-bot systems evolve constantly. What works now may trigger blocks in months. Stay current with detection techniques and update your approach accordingly.

Key takeaways:

- **Randomize everything**: Timing, mouse paths, typing speeds, viewports - variation defeats pattern recognition
- **Combine multiple techniques**: Humanized typing alone isn't enough; layer stealth plugins, mouse movements, and proxy rotation
- **Test regularly**: Run bot detection tests to catch regressions before they affect production
- **Respect rate limits**: Aggressive scraping gets noticed; patient scraping succeeds

Happy scraping - and stay under the radar.