Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost Your Puppeteer script works flawlessly on your MacBook. Push to prod? Instant Cloudflare ban. Install headless Chrome on your server? Welcome to dependency hell. Steel gives you cloud browsers that just work; in Node, Deno, Bun, or wherever JavaScript runs these days.

When localhost:9222 Isn't Enough Anymore

Picture this: It's 3am. Your e-commerce scraper is down. Again. The Docker container crashed because Chrome ate all the RAM. Your proxy provider ghosted you. The site you're scraping now requires you to solve a picture puzzle of traffic lights.

You open GitHub and see 47 open issues on puppeteer-extra-plugin-stealth. The last commit was 8 months ago.

There has to be a better way.

Enter Steel: Cloud browsers with a proper API. No containers. No plugins. Just browsers that don't get detected.

Zero to Browser in 30 Seconds

npm

No post-install scripts downloading 300MB of Chromium. No "please install these fonts for emoji support." Just a lightweight SDK with minimal dependencies.

Here's your first cloud browser:

import Steel from 'steel-sdk';

async function main() {
  try {
    const steel = new Steel(); // reads STEEL_API_KEY from env
    
    const session = await steel.sessions.create();
    console.log(`Session ID: ${session.id}`);
    console.log(`Watch live: ${session.sessionViewerUrl}`);
    console.log(`WebSocket: ${session.websocketUrl}`);
    
    // That's it. You have a browser. In the cloud. Ready to connect.
    
    // Always clean up
    await steel.sessions.release(session.id);
    
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();

The TypeScript Experience We Deserve

Remember wrestling with Puppeteer's types? Page | null | undefined | ElementHandle<Element | null> | void?

Steel's SDK is TypeScript-first with types that actually help:

import Steel from 'steel-sdk';
import type { Session, SessionCreateParams } from 'steel-sdk';

// Every option, fully typed
const config: SessionCreateParams = {
  dimensions: {                // ✓ viewport config
    width: 1920,
    height: 1080
  },
  timeout: 600000,             // ✓ session duration (10 min)
  sessionContext: {            // ✓ preload cookies & storage
    cookies: [/* ... */],
    localStorage: {/* ... */}
  },
  // Pro features (when you upgrade):
  useProxy: true,              // residential proxies
  solveCaptcha: true,          // beats reCAPTCHA
  region: 'us-east'            // geographic targeting
};

// Autocomplete everywhere
const session = await steel.sessions.create(config);
session.// ← Your IDE knows everything

No more @ts-ignore. No more "I hope this property exists." Just types that work.

Puppeteer, But Make It Cloud Native

Your existing Puppeteer code? It just works:

import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer
import Steel from 'steel-sdk';

async function scrapeWithStyle() {
  const steel = new Steel();
  
  // Get a cloud browser with all protections
  const session = await steel.sessions.create({
    useProxy: true,
    solveCaptcha: true
  });
  
  // Connect Puppeteer like nothing changed
  const browser = await puppeteer.connect({
    browserWSEndpoint: session.websocketUrl
  });
  
  // Your code runs exactly the same
  const page = await browser.newPage();
  await page.goto('https://bot-protected-site.com');
  
  // Except now it doesn't get blocked 🎉
  const data = await page.evaluate(() => {
    return document.querySelector('.price')?.textContent;
  });
  
  // Always clean up
  await steel.sessions.release(session.id);
  
  return data;
}

Same API. Better infrastructure. Zero learning curve.

Works With Your Entire Stack

Playwright? Obviously.

import { chromium } from 'playwright';
import Steel from 'steel-sdk';

async function usePlaywright() {
  const steel = new Steel();
  const session = await steel.sessions.create();

  try {
    const browser = await chromium.connectOverCDP(session.websocketUrl);
    const context = await browser.newContext();
    const page = await context.newPage();
    
    // Now use Playwright as normal
    await page.goto('https://example.com');
    const title = await page.title();
    console.log(`Title: ${title}`);
    
    await browser.close();
  } finally {
    await steel.sessions.release(session.id);
  }
}

Running on Vercel Edge? Cloudflare Workers? No problem.

// edge-function.ts
import Steel from 'steel-sdk';

export default async function handler(req: Request) {
  const steel = new Steel({
    steelAPIKey: process.env.STEEL_API_KEY
  });
  
  const session = await steel.sessions.create();
  // Steel handles the WebSocket → HTTP bridge automatically
  
  return Response.json({ 
    sessionId: session.id,
    viewerUrl: session.sessionViewerUrl 
  });
}

Bun? Deno? We don't discriminate.

// Works in Bun
import Steel from 'steel-sdk';

// Works in Deno
import Steel from 'npm:steel-sdk';

// Same API, same features, everywhere

Session State That Persists (Like a Real Browser)

Remember implementing your own cookie jar? Managing localStorage across requests? Rebuilding browser state from scratch?

Steel sessions are stateful by default:

// Create a session with existing context
const session = await steel.sessions.create({
  sessionContext: {
    cookies: savedCookies,
    localStorage: {
      'example.com': {
        'auth_token': 'abc123',
        'user_prefs': JSON.stringify({ theme: 'dark' })
      }
    }
  }
});

// ... do your thing ...

// Save the context for next time
const context = await steel.sessions.context(session.id);
await saveToDatabase({
  cookies: context.cookies,
  localStorage: context.localStorage,
  sessionStorage: context.sessionStorage
});

Sessions last up to 24 hours. Perfect for long-running automations, multi-step workflows, or just keeping that login alive.

When Your Automation Needs Files

Upload CSVs, download reports, handle PDFs, without the fs.writeFileSync dance:

// Upload a file to the session
const csv = await steel.sessions.files.upload(session.id, {
  file: Buffer.from('name,email\njohn,john@example.com'),
  path: 'data/users.csv'
});

// Your browser can now access it at file:///data/users.csv
await page.goto('file:///data/users.csv');

// Download files the browser created
const screenshot = await page.screenshot();
await steel.sessions.files.upload(session.id, {
  file: screenshot,
  path: 'screenshots/homepage.png'
});

// Get everything as a zip
const archive = await steel.sessions.files.downloadArchive(session.id);

No temp directories. No cleanup scripts. Just files that work.

Error Handling for the Real World

Because UnhandledPromiseRejectionWarning: Error: Protocol error (Target.createTarget): Target closed isn't helpful:

import Steel from 'steel-sdk';

class ResilientScraper {
  private steel: Steel;
  
  constructor() {
    this.steel = new Steel();
  }
  
  async scrapeWithRetry(url: string, maxAttempts = 3) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      const session = await this.createSessionWithBackoff(attempt);
      
      try {
        const result = await this.performScrape(session, url);
        return result;
        
      } catch (error) {
        // Actual useful error types
        if (error instanceof Steel.RateLimitError) {
          console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`);
          await this.wait(parseInt(error.headers['retry-after']) * 1000);
          
        } else if (error instanceof Steel.APIConnectionTimeoutError) {
          console.log(`Timeout on attempt ${attempt}, retrying...`);
          
        } else if (error instanceof Steel.BadRequestError) {
          // Don't retry invalid requests
          throw new Error(`Invalid request: ${error.message}`);
        }
        
      } finally {
        // Always cleanup
        await this.steel.sessions.release(session.id).catch(() => {});
      }
    }
    
    throw new Error(`Failed after ${maxAttempts} attempts`);
  }
  
  private async createSessionWithBackoff(attempt: number) {
    const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
    if (attempt > 1) await this.wait(delay);
    
    return this.steel.sessions.create({
      useProxy: true,
      solveCaptcha: true
    });
  }
  
  private wait = (ms: number) => new Promise(r => setTimeout(r, ms));
}

Real error types. Real retry strategies. Real production code.

The Features That Make You Go "Finally!"

Geographic Precision

const session = await steel.sessions.create({
  useProxy: {
    geolocation: {
      country: 'US',
      state: 'NY',
      city: 'NEW_YORK'
    }
  }
});
// Your browser is now a New Yorker 🗽

Human-Like Behavior

const session = await steel.sessions.create({
  stealthConfig: {
    humanizeInteractions: true // Mouse movements that don't scream "BOT"
  }
});

Auto-CAPTCHA Solving

// Encounters reCAPTCHA? No problem.
const session = await steel.sessions.create({
  solveCaptcha: true
});

// Check CAPTCHA status
const status = await steel.sessions.captchas.status(session.id);

Session Inspection

// Watch your automation live (seriously, try this)
console.log(`Debug live: ${session.sessionViewerUrl}`);

// Get all session events for playback
const events = await steel.sessions.events(session.id);

Pricing That Doesn't Require a VC Round

Hobby (Free): 100 browser hours/month

  • Perfect for side projects

  • Basic anti-detection

  • Community Discord access

Pro: Pay for what you use

  • Residential proxies

  • CAPTCHA solving

  • Geographic targeting

  • Priority support

Enterprise: Let's talk

  • Custom proxy pools

  • Dedicated infrastructure

  • SLAs that mean something

  • A Slack channel where we actually respond

Migration Guide: From Localhost to Cloud

Got an existing Puppeteer project? Here's your 5-minute migration:

Before (Your Current Setup)

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// ... your automation ...
await browser.close();

After (With Steel)

import puppeteer from 'puppeteer-core';
import Steel from 'steel-sdk';

const steel = new Steel();
const session = await steel.sessions.create();

const browser = await puppeteer.connect({
  browserWSEndpoint: session.websocketUrl
});
const page = await browser.newPage();
// ... your automation (unchanged!) ...
await steel.sessions.release(session.id);

That's it. Your code stays the same. Your success rate goes through the roof.

Join our Discord where people actually help each other instead of just posting memes (okay, we post memes too).

Let's Ship Something

# Install
npm install steel-sdk puppeteer-core dotenv

# Set your API key (get one at app.steel.dev)
echo "STEEL_API_KEY=your_api_key_here" > .env

# Run this quickstart

// quickstart.ts
import 'dotenv/config';
import Steel from 'steel-sdk';
import puppeteer from 'puppeteer-core';

const steel = new Steel();

async function main() {
  let session;
  
  try {
    // Create a session
    session = await steel.sessions.create();

    console.log(`
🚀 Session ready!
📺 Watch live: ${session.sessionViewerUrl}
🔗 Session ID: ${session.id}
`);

    // Connect and scrape
    const browser = await puppeteer.connect({
      browserWSEndpoint: session.websocketUrl
    });

    const page = await browser.newPage();
    await page.goto('https://example.com');
    const title = await page.title();

    console.log(`\n✅ Scraped title: ${title}`);

    await browser.close();

  } catch (error) {
    console.error('Error:', error);
    process.exit(1);
  } finally {
    // Always cleanup
    if (session) {
      await steel.sessions.release(session.id);
      console.log('\n🧹 Session released. Happy scraping!');
    }
  }
}

main();

Resources for the Curious

The Part Where We Get Real

Browser automation is hard. It shouldn't be. You came here to build something cool, not to become a Docker expert or a proxy specialist.

Steel handles the infrastructure so you can focus on the fun part, building things that matter.

No more "works on my machine." No more random bans. No more 3am debugging sessions.

Just npm install steel-sdk and start shipping.

Ready? Grab your API key and let's build something the internet hasn't seen before.

P.S. - If you build something cool, tell us in Discord. We love seeing what you ship with Steel. Plus, we might feature it (with your permission) and send you swag.

Jul 20, 2025

Jul 20, 2025

Jul 20, 2025

Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost

Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost

Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost

Steel Node SDK: Cloud browsers in 3 lines of code. Works with Puppeteer, Playwright. No Docker hell. TypeScript-first. Free 100hrs/mo.

Ready to

Build with Steel?

Ready to

Build with Steel?

Ready to

Build with Steel?

Ready to Build with Steel?

A better way to take your LLMs online.

© Steel · Inc. 2024.

All Systems Operational

Platform

Join the community