Bots get caught because they type too perfectly. Real humans pause, make mistakes, and type at irregular speeds.

Human typing in Playwright simulates natural keyboard input by adding random delays between keystrokes, mimicking the irregular rhythm of actual human typing. This guide covers three approaches: using Playwright's built-in delay option, creating custom typing functions with variable timing, and implementing advanced patterns with typos and corrections.

Why Human Typing Matters for Web Automation

Bot detection systems analyze typing patterns. They look at keystroke timing, consistency, and rhythm.

When you use page.fill() or page.type() without delays, every character appears instantly. This screams "automated script" to any detection system worth its salt.

Real humans don't type like that. We pause to think. We hit keys at different speeds depending on finger position. We sometimes hesitate before hitting unfamiliar characters.

Anti-bot systems track these patterns:

  • Time between keystrokes (inter-key delay)
  • Consistency of typing speed
  • Pauses at word boundaries
  • Overall typing rhythm

By mimicking these patterns, your Playwright scripts become much harder to detect.

Method 1: Use Playwright's Built-in Delay Option

Playwright's type() method accepts a delay parameter. This is the simplest approach.

The delay adds a fixed pause between each keystroke. It's not perfect—real humans don't type at constant speeds—but it's better than instant typing.

Basic Implementation

const { chromium } = require('playwright');

async function typeWithDelay() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.goto('https://example.com/login');
  
  // Type with 100ms delay between each character
  await page.type('#username', 'myusername', { delay: 100 });
  await page.type('#password', 'mypassword', { delay: 100 });
  
  await browser.close();
}

typeWithDelay();

This code launches a browser and navigates to a login page. The delay: 100 option tells Playwright to wait 100 milliseconds between each keystroke.

Adjusting the Delay Value

Different delay values create different typing speeds:

  • 50ms: Fast typist (~120 WPM)
  • 100ms: Average typist (~60 WPM)
  • 150ms: Slow typist (~40 WPM)
  • 200ms+: Hunt-and-peck typing

Pick a value that matches your use case. For sensitive forms, slower is usually safer.

The Problem with Fixed Delays

Fixed delays are predictable. Detection systems can identify the pattern.

If every keystroke takes exactly 100ms, that's still unnatural. Real typing has variance.

This brings us to method 2.

Method 2: Build a Custom Human Typing Function

A custom function lets you add randomness to typing speed. This better simulates actual human behavior.

The Core Concept

Instead of a fixed delay, we'll use a random delay within a range. Each keystroke gets a different timing.

function getRandomDelay(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

This helper function returns a random integer between min and max. We'll use it to vary our keystroke timing.

Complete Human Typing Function

async function humanType(page, selector, text, options = {}) {
  const {
    minDelay = 50,
    maxDelay = 150,
    mistakeChance = 0  // We'll use this in Method 3
  } = options;
  
  // Click the input field first
  await page.click(selector);
  
  // Type each character with random delay
  for (const char of text) {
    await page.keyboard.type(char);
    const delay = getRandomDelay(minDelay, maxDelay);
    await page.waitForTimeout(delay);
  }
}

The function accepts a page object, CSS selector, text to type, and optional configuration. It clicks the target element, then types one character at a time with random delays.

Using the Function

const { chromium } = require('playwright');

async function main() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.goto('https://example.com/signup');
  
  // Type with human-like timing
  await humanType(page, '#email', 'user@example.com', {
    minDelay: 80,
    maxDelay: 200
  });
  
  await humanType(page, '#password', 'SecurePass123!', {
    minDelay: 100,
    maxDelay: 250
  });
  
  await browser.close();
}

main();

Notice the different delay ranges. The password field uses slower typing—people often type passwords more carefully.

Adding Word Boundary Pauses

Humans naturally pause between words. Adding this behavior increases realism.

async function humanTypeWithPauses(page, selector, text, options = {}) {
  const {
    minDelay = 50,
    maxDelay = 150,
    wordPauseMin = 200,
    wordPauseMax = 500
  } = options;
  
  await page.click(selector);
  
  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    await page.keyboard.type(char);
    
    // Check if we just typed a space (word boundary)
    if (char === ' ') {
      const pause = getRandomDelay(wordPauseMin, wordPauseMax);
      await page.waitForTimeout(pause);
    } else {
      const delay = getRandomDelay(minDelay, maxDelay);
      await page.waitForTimeout(delay);
    }
  }
}

This version adds longer pauses after spaces. It simulates the brief hesitation between words.

Method 3: Add Realistic Typos and Corrections

Real humans make mistakes. Adding occasional typos (and corrections) makes your automation even more convincing.

The Typo Strategy

We'll occasionally type a wrong character, pause, delete it, and type the correct one. This mimics natural typing errors.

function getAdjacentKey(char) {
  const keyboard = {
    'q': ['w', 'a'],
    'w': ['q', 'e', 's'],
    'e': ['w', 'r', 'd'],
    'r': ['e', 't', 'f'],
    't': ['r', 'y', 'g'],
    'y': ['t', 'u', 'h'],
    'u': ['y', 'i', 'j'],
    'i': ['u', 'o', 'k'],
    'o': ['i', 'p', 'l'],
    'p': ['o', '[', ';'],
    'a': ['q', 's', 'z'],
    's': ['a', 'w', 'd', 'x'],
    'd': ['s', 'e', 'f', 'c'],
    'f': ['d', 'r', 'g', 'v'],
    'g': ['f', 't', 'h', 'b'],
    'h': ['g', 'y', 'j', 'n'],
    'j': ['h', 'u', 'k', 'm'],
    'k': ['j', 'i', 'l', ','],
    'l': ['k', 'o', ';', '.'],
    'z': ['a', 'x'],
    'x': ['z', 's', 'c'],
    'c': ['x', 'd', 'v'],
    'v': ['c', 'f', 'b'],
    'b': ['v', 'g', 'n'],
    'n': ['b', 'h', 'm'],
    'm': ['n', 'j', ',']
  };
  
  const lowerChar = char.toLowerCase();
  const adjacent = keyboard[lowerChar];
  
  if (!adjacent) return null;
  
  return adjacent[Math.floor(Math.random() * adjacent.length)];
}

This function maps each key to its physical neighbors on a QWERTY keyboard. When we make a "typo," we'll hit an adjacent key—just like a real mistake.

Complete Implementation with Typos

async function humanTypeWithMistakes(page, selector, text, options = {}) {
  const {
    minDelay = 50,
    maxDelay = 150,
    mistakeChance = 0.05,  // 5% chance of typo
    correctionDelay = 300  // Pause before correcting
  } = options;
  
  await page.click(selector);
  
  for (const char of text) {
    // Decide if we should make a typo
    if (Math.random() < mistakeChance) {
      const wrongKey = getAdjacentKey(char);
      
      if (wrongKey) {
        // Type the wrong character
        await page.keyboard.type(wrongKey);
        await page.waitForTimeout(getRandomDelay(100, 200));
        
        // Pause like we noticed the mistake
        await page.waitForTimeout(correctionDelay);
        
        // Delete the wrong character
        await page.keyboard.press('Backspace');
        await page.waitForTimeout(getRandomDelay(50, 100));
      }
    }
    
    // Type the correct character
    await page.keyboard.type(char);
    await page.waitForTimeout(getRandomDelay(minDelay, maxDelay));
  }
}

This function introduces a configurable chance of making typos. When a typo occurs, it types an adjacent key, pauses (simulating recognition of the error), deletes it, and then types the correct character.

Practical Usage

const { chromium } = require('playwright');

async function fillFormWithHumanTyping() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.goto('https://example.com/register');
  
  // Name field - moderate typo chance
  await humanTypeWithMistakes(page, '#name', 'John Smith', {
    minDelay: 60,
    maxDelay: 140,
    mistakeChance: 0.03
  });
  
  // Email field - fewer typos (we're more careful)
  await humanTypeWithMistakes(page, '#email', 'john.smith@example.com', {
    minDelay: 80,
    maxDelay: 180,
    mistakeChance: 0.02
  });
  
  // Password - slowest, most careful
  await humanTypeWithMistakes(page, '#password', 'MySecureP@ss123', {
    minDelay: 120,
    maxDelay: 250,
    mistakeChance: 0.01
  });
  
  await browser.close();
}

fillFormWithHumanTyping();

Different fields get different configurations. This reflects how humans actually type—more carefully on important fields.

Method 4: Gaussian Distribution for Natural Timing

Random delays are good. But truly natural typing follows a bell curve distribution.

Most keystrokes fall near an average speed. Occasional keystrokes are much faster or slower.

Understanding Gaussian Distribution

A uniform random distribution gives equal probability to all delays. A Gaussian (normal) distribution clusters most values around the mean.

This better matches real typing patterns.

function gaussianRandom(mean, standardDeviation) {
  // Box-Muller transform for Gaussian distribution
  let u1 = Math.random();
  let u2 = Math.random();
  
  // Avoid log(0)
  while (u1 === 0) u1 = Math.random();
  
  const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
  return z * standardDeviation + mean;
}

This implements the Box-Muller transform. It converts uniform random numbers into a Gaussian distribution.

The mean parameter sets the center of the distribution. The standardDeviation controls the spread.

Gaussian Human Typing Function

async function gaussianHumanType(page, selector, text, options = {}) {
  const {
    meanDelay = 100,
    standardDeviation = 30,
    minDelay = 30,
    maxDelay = 300
  } = options;
  
  await page.click(selector);
  
  for (const char of text) {
    await page.keyboard.type(char);
    
    // Generate Gaussian-distributed delay
    let delay = gaussianRandom(meanDelay, standardDeviation);
    
    // Clamp to reasonable bounds
    delay = Math.max(minDelay, Math.min(maxDelay, delay));
    delay = Math.floor(delay);
    
    await page.waitForTimeout(delay);
  }
}

Most delays will cluster around meanDelay. The standardDeviation determines how much variation exists.

Full Example with Gaussian Timing

const { chromium } = require('playwright');

function gaussianRandom(mean, standardDeviation) {
  let u1 = Math.random();
  let u2 = Math.random();
  while (u1 === 0) u1 = Math.random();
  const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
  return z * standardDeviation + mean;
}

async function gaussianHumanType(page, selector, text, options = {}) {
  const {
    meanDelay = 100,
    standardDeviation = 30,
    minDelay = 30,
    maxDelay = 300
  } = options;
  
  await page.click(selector);
  
  for (const char of text) {
    await page.keyboard.type(char);
    
    let delay = gaussianRandom(meanDelay, standardDeviation);
    delay = Math.max(minDelay, Math.min(maxDelay, delay));
    delay = Math.floor(delay);
    
    await page.waitForTimeout(delay);
  }
}

async function main() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.goto('https://example.com/form');
  
  await gaussianHumanType(page, '#search', 'playwright human typing tutorial', {
    meanDelay: 90,
    standardDeviation: 25
  });
  
  await browser.close();
}

main();

The Gaussian approach produces the most realistic timing distribution.

Combining Human Typing with Other Anti-Detection Techniques

Human typing alone won't bypass sophisticated detection. Combine it with these techniques.

Use Realistic Browser Fingerprints

Detection systems check browser properties. Use playwright-extra with stealth plugins.

const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();

chromium.use(stealth);

async function stealthTyping() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    locale: 'en-US',
    timezoneId: 'America/New_York'
  });
  
  const page = await context.newPage();
  // Now use human typing functions...
}

The stealth plugin patches many automation fingerprints. Combined with realistic context settings, it significantly improves detection evasion.

Add Mouse Movement Before Typing

Real users move their mouse to input fields before typing.

async function moveAndType(page, selector, text, options) {
  // Get element position
  const element = await page.$(selector);
  const box = await element.boundingBox();
  
  // Move mouse to element with slight randomness
  const x = box.x + box.width / 2 + (Math.random() * 10 - 5);
  const y = box.y + box.height / 2 + (Math.random() * 10 - 5);
  
  await page.mouse.move(x, y, { steps: 25 });
  await page.waitForTimeout(getRandomDelay(100, 300));
  
  // Click and type
  await page.mouse.click(x, y);
  await humanTypeWithMistakes(page, selector, text, options);
}

This function moves the mouse cursor to the input field before typing. The steps: 25 creates a smooth, natural-looking movement path.

Use Proxies for IP Rotation

Multiple requests from one IP trigger detection. Rotate proxies to appear as different users.

Residential proxies work best for this purpose. They use real ISP-assigned IPs that look like normal user traffic.

const { chromium } = require('playwright');

async function typingWithProxy() {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://proxy.example.com:8080',
      username: 'user',
      password: 'pass'
    }
  });
  
  const page = await browser.newPage();
  // Continue with human typing...
}

Common Mistakes to Avoid

These errors undermine your human typing implementation.

Using Consistent Timing Across All Fields

Don't use the same delay settings everywhere. Vary your configuration per field type.

Passwords should be slower. Frequently-typed information (like email) can be faster.

Forgetting to Click Before Typing

Some implementations use page.keyboard.type() without clicking the field first. Always click or focus the target element.

// Wrong - might type in wrong place
await page.keyboard.type('username');

// Correct - ensures focus
await page.click('#username');
await page.keyboard.type('username');

Making Delay Ranges Too Wide

A delay range of 10-1000ms is unrealistic. No human varies that much.

Keep ranges reasonable: 50-150ms for fast typing, 100-250ms for careful typing.

Ignoring Special Characters

Special characters take longer to type. They require shift key combinations or number row reaches.

function getCharacterDelay(char, baseDelay) {
  const specialChars = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\`~';
  const numbers = '0123456789';
  
  if (specialChars.includes(char)) {
    return baseDelay * 1.5;  // 50% slower for special chars
  }
  
  if (numbers.includes(char)) {
    return baseDelay * 1.2;  // 20% slower for numbers
  }
  
  return baseDelay;
}

Integrate this into your typing function for more realistic special character timing.

Testing Your Human Typing Implementation

Verify your implementation actually looks human.

Visual Inspection

Run with headless: false and watch the typing. Does it look natural?

Look for these patterns:

  • Irregular keystroke rhythm
  • Natural pauses at word boundaries
  • Believable typing speed

Timing Analysis

Log your delays and analyze the distribution.

async function analyzedHumanType(page, selector, text, options = {}) {
  const delays = [];
  
  await page.click(selector);
  
  for (const char of text) {
    const delay = getRandomDelay(options.minDelay || 50, options.maxDelay || 150);
    delays.push(delay);
    
    await page.keyboard.type(char);
    await page.waitForTimeout(delay);
  }
  
  // Log statistics
  const avg = delays.reduce((a, b) => a + b, 0) / delays.length;
  const min = Math.min(...delays);
  const max = Math.max(...delays);
  
  console.log(`Delay stats - Avg: ${avg}ms, Min: ${min}ms, Max: ${max}ms`);
  
  return delays;
}

Check that your statistics show reasonable variance.

Bot Detection Test Sites

Test against known bot detection services:

  • reCAPTCHA demo page: See if you trigger challenges
  • Bot detection test sites: Several exist specifically for testing
  • Target site staging: If available, test against your actual target

FAQ

What delay should I use for human typing in Playwright?

Use delays between 50-200ms for realistic human typing in Playwright. Most people type at 40-80 words per minute, which translates to roughly 80-150ms per character. Add variance—don't use fixed delays.

Does Playwright have built-in human typing?

Playwright's type() method includes a delay option for basic human typing simulation. However, this uses fixed delays. For better results, build a custom function with random delays and occasional typos.

Can human typing bypass all bot detection?

No. Human typing in Playwright is one part of a multi-layered approach. Sophisticated detection also checks browser fingerprints, mouse movements, IP reputation, and behavioral patterns. Combine typing simulation with stealth plugins and realistic browser contexts.

Should I add typos to my human typing simulation?

Optional but effective. Adding occasional typos (2-5% chance) with corrections makes human typing in Playwright more believable. Map typos to adjacent keyboard keys for realism.

How do I test if my human typing looks realistic?

Run with headless: false and watch the typing visually. Log delay statistics to verify variance. Test against bot detection demo pages. Compare your timing distribution against real human typing studies.

Wrapping Up

Setting up human typing in Playwright comes down to adding realistic delays and variance to your keystrokes.

Start with Playwright's built-in delay option for quick implementation. Graduate to custom functions with random delays for better results. Add Gaussian distribution and typos for maximum realism.

Remember: human typing alone won't defeat all detection. Combine it with stealth plugins, realistic browser contexts, mouse movements, and proxy rotation.

Test your implementation. Watch it run. Adjust until it looks natural.

The best automation is invisible automation.