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
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();
const session = await steel.sessions.create();
console.log(`Session ID: ${session.id}`);
console.log(`Watch live: ${session.sessionViewerUrl}`);
console.log(`WebSocket: ${session.websocketUrl}`);
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';
const config: SessionCreateParams = {
dimensions: {
width: 1920,
height: 1080
},
timeout: 600000,
sessionContext: {
cookies: [],
localStorage: {}
},
useProxy: true,
solveCaptcha: true,
region: 'us-east'
};
const session = await steel.sessions.create(config);
session.
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';
import Steel from 'steel-sdk';
async function scrapeWithStyle() {
const steel = new Steel();
const session = await steel.sessions.create({
useProxy: true,
solveCaptcha: true
});
const browser = await puppeteer.connect({
browserWSEndpoint: session.websocketUrl
});
const page = await browser.newPage();
await page.goto('https://bot-protected-site.com');
const data = await page.evaluate(() => {
return document.querySelector('.price')?.textContent;
});
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();
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.
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();
return Response.json({
sessionId: session.id,
viewerUrl: session.sessionViewerUrl
});
}
Bun? Deno? We don't discriminate.
import Steel from 'steel-sdk';
import Steel from 'npm:steel-sdk';
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:
const session = await steel.sessions.create({
sessionContext: {
cookies: savedCookies,
localStorage: {
'example.com': {
'auth_token': 'abc123',
'user_prefs': JSON.stringify({ theme: 'dark' })
}
}
}
});
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:
const csv = await steel.sessions.files.upload(session.id, {
file: Buffer.from('name,email\njohn,john@example.com'),
path: 'data/users.csv'
});
await page.goto('file:///data/users.csv');
const screenshot = await page.screenshot();
await steel.sessions.files.upload(session.id, {
file: screenshot,
path: 'screenshots/homepage.png'
});
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) {
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) {
throw new Error(`Invalid request: ${error.message}`);
}
} finally {
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'
}
}
});
Human-Like Behavior
const session = await steel.sessions.create({
stealthConfig: {
humanizeInteractions: true
}
});
Auto-CAPTCHA Solving
const session = await steel.sessions.create({
solveCaptcha: true
});
const status = await steel.sessions.captchas.status(session.id);
Session Inspection
console.log(`Debug live: ${session.sessionViewerUrl}`);
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();
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();
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
npm install steel-sdk puppeteer-core dotenv
echo "STEEL_API_KEY=your_api_key_here" > .env
import 'dotenv/config';
import Steel from 'steel-sdk';
import puppeteer from 'puppeteer-core';
const steel = new Steel();
async function main() {
let session;
try {
session = await steel.sessions.create();
console.log(`
🚀 Session ready!
📺 Watch live: ${session.sessionViewerUrl}
🔗 Session ID: ${session.id}
`);
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 {
if (session) {
await steel.sessions.release(session.id);
console.log('\n🧹 Session released. Happy scraping!');
}
}
}
main();
Resources for the Curious
GitHub - Star it, fork it, break it, fix it
API Docs - Every endpoint, every option
Steel Cookbook - Copy-paste-ship recipes
System Status - We're transparent about uptime
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.