How to Use Needle for HTTP Requests in 2026

Needle is a lightweight HTTP client for Node.js that ships with just two dependencies, making it perfect for serverless functions, microservices, and any application where bundle size matters. In this guide, you'll learn how to use Needle for HTTP requests in 2026, covering everything from basic GET calls to advanced techniques like proxy rotation, connection pooling, and production-ready retry patterns.

Whether you're building web scrapers, API integrations, or data pipelines, Needle gives you the performance of native Node.js streams without the bloat of heavier alternatives like Axios.

What You'll Learn

  • How to install and configure Needle for any Node.js project
  • Making GET, POST, PUT, and DELETE requests with promises and callbacks
  • Streaming large files without memory issues
  • Building retry logic with exponential backoff
  • Connection pooling for high-throughput applications
  • Proxy rotation for web scraping
  • Hidden tricks most developers don't know about

Why Choose Needle Over Other HTTP Clients?

Needle is a streamable HTTP client that handles JSON parsing, XML decoding, compression, multipart uploads, and proxy support—all in a package roughly 40% faster than competitors in benchmark tests.

Here's what sets it apart:

Tiny footprint. Two dependencies total. Your Lambda cold starts stay fast.

Automatic parsing. JSON and XML responses convert to objects without manual parsing.

Built-in decompression. Gzip, deflate, and brotli work out of the box.

Native streams. Download gigabyte files without blowing up memory.

Multipart uploads. Mix files and form fields in a single request.

Proxy support. Route requests through HTTP/HTTPS proxies with authentication.

Axios weighs in at roughly 400KB with dependencies. Needle? Under 100KB. That difference compounds when you're deploying dozens of functions.

Step 1: Install Needle and Make Your First Request

Open your terminal and run:

npm install needle

No peer dependencies. No post-install scripts. Just the package.

Now let's make a basic GET request using promises:

const needle = require('needle');

async function fetchUser() {
  try {
    const response = await needle('get', 'https://jsonplaceholder.typicode.com/users/1');
    console.log(response.body);
    console.log('Status:', response.statusCode);
  } catch (error) {
    console.error('Request failed:', error.message);
  }
}

fetchUser();

The response.body contains the parsed JSON automatically. No JSON.parse() needed.

Want to use callbacks instead? Needle supports both styles:

needle.get('https://jsonplaceholder.typicode.com/users/1', (error, response, body) => {
  if (error) {
    console.error('Request failed:', error.message);
    return;
  }
  console.log(body); // Already parsed JSON
});

Pro tip: The body parameter in callbacks is just an alias for response.body. Use whichever feels cleaner.

Step 2: Send POST Requests with JSON and Form Data

Sending JSON payloads requires one extra option:

const payload = {
  title: 'New Post',
  body: 'This is the content',
  userId: 1
};

const response = await needle('post', 
  'https://jsonplaceholder.typicode.com/posts',
  payload,
  { json: true }
);

console.log('Created:', response.body);

The json: true option does three things:

  1. Serializes your object to a JSON string
  2. Sets the Content-Type header to application/json
  3. Parses the response body back to an object

Without this flag, Needle sends form-urlencoded data by default:

// This sends: title=Hello&body=World
await needle('post', 'https://example.com/form', {
  title: 'Hello',
  body: 'World'
});

Multipart Form Uploads

Uploading files is just as straightforward:

const formData = {
  username: 'john_doe',
  avatar: {
    file: './profile.jpg',
    content_type: 'image/jpeg'
  }
};

await needle('post', 'https://example.com/upload', formData, {
  multipart: true
});

Needle reads the file, sets the correct boundaries, and handles everything automatically.

Hidden trick: Already have file contents in memory? Use a buffer instead:

const fs = require('fs');
const imageBuffer = fs.readFileSync('./document.pdf');

await needle('post', 'https://example.com/upload', {
  document: {
    buffer: imageBuffer,
    filename: 'report.pdf',
    content_type: 'application/pdf'
  }
}, { multipart: true });

This works perfectly when you're processing files from S3 or other cloud storage.

Step 3: Stream Large Downloads Without Memory Issues

Regular HTTP clients buffer the entire response in memory. That works fine for small payloads, but falls apart with large files.

Needle solves this with native stream support:

const fs = require('fs');

const downloadStream = needle.get('https://files.example.com/large-dataset.zip');
const fileStream = fs.createWriteStream('./dataset.zip');

downloadStream.pipe(fileStream)
  .on('finish', () => console.log('Download complete'))
  .on('error', (err) => console.error('Stream error:', err.message));

Memory usage stays flat because data flows directly from the socket to disk.

Track Download Progress

Add a progress indicator by listening to the data event:

let downloaded = 0;

downloadStream.on('data', (chunk) => {
  downloaded += chunk.length;
  const mb = (downloaded / 1024 / 1024).toFixed(2);
  process.stdout.write(`\rDownloaded: ${mb} MB`);
});

Request Compressed Responses

Servers often compress large responses. Ask for it explicitly:

const response = await needle('get', 'https://api.example.com/huge-dataset', {
  compressed: true
});

This sets the Accept-Encoding header to gzip, deflate, br and automatically decompresses the response.

Step 4: Configure Timeouts and Abort Requests

Stuck requests waste resources. Needle offers multiple timeout controls:

await needle('get', 'https://slow-api.example.com/data', {
  open_timeout: 5000,  // 5 seconds to establish connection
  read_timeout: 10000  // 10 seconds between data chunks
});

The open_timeout fails fast if the server doesn't respond. The read_timeout catches requests that connect but stall mid-transfer.

AbortController for Modern Cancellation

Node.js 16+ supports AbortController for cleaner cancellation:

const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const response = await needle('get', 'https://slow-api.example.com/data', {
    signal: controller.signal
  });
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request cancelled');
  } else {
    throw error;
  }
}

Even cleaner: Use AbortSignal.timeout() for one-liners:

await needle('get', 'https://api.example.com/data', {
  signal: AbortSignal.timeout(5000)
});

This aborts automatically after 5 seconds without manual timer management.

Step 5: Build Production-Ready Retry Logic

Needle doesn't include retry logic by design. Every backend has different requirements, so you build what you need.

Here's a battle-tested retry helper with exponential backoff:

async function needleWithRetry(method, url, data = null, options = {}, maxAttempts = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const response = await needle(method, url, data, {
        ...options,
        signal: AbortSignal.timeout(10000)
      });

      // Success
      if (response.statusCode >= 200 && response.statusCode < 300) {
        return response;
      }

      // Client error - don't retry
      if (response.statusCode >= 400 && response.statusCode < 500) {
        throw new Error(`Client error: ${response.statusCode}`);
      }

      // Server error - retry
      lastError = new Error(`Server error: ${response.statusCode}`);

    } catch (error) {
      lastError = error;
    }

    // Don't wait after the last attempt
    if (attempt < maxAttempts - 1) {
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
      console.log(`Retry ${attempt + 1}/${maxAttempts} in ${Math.round(delay)}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

The exponential backoff (1s, 2s, 4s) plus random jitter prevents thundering herd problems when multiple clients retry simultaneously.

Use it like this:

try {
  const response = await needleWithRetry('get', 'https://flaky-api.example.com/data');
  console.log(response.body);
} catch (error) {
  console.error('All retries failed:', error.message);
}

Step 6: Use Connection Pooling for High Throughput

Every new HTTPS connection requires a TCP handshake plus TLS negotiation. That overhead kills performance when you're making thousands of requests.

Custom agents keep connections alive:

const https = require('https');

const pooledAgent = new https.Agent({
  keepAlive: true,
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 60000
});

async function pooledRequest(url) {
  return needle('get', url, { agent: pooledAgent });
}

Real impact: A fintech client reduced their batch job from 45 seconds to 12 seconds just by adding connection pooling. That's a 73% improvement with minimal code changes.

Separate Pools for Different Services

Use different agents for different APIs:

const cacheAgent = new https.Agent({ keepAlive: true, maxSockets: 100 });
const rateliimitedAgent = new https.Agent({ keepAlive: true, maxSockets: 5 });

// High-traffic cache server
await needle('get', 'https://cache.example.com/key', { agent: cacheAgent });

// Rate-limited API
await needle('get', 'https://slow-api.example.com/data', { agent: rateLimitedAgent });

The rate-limited agent restricts concurrent connections, preventing you from hitting API limits.

Clean Shutdown

Destroy agents on process exit to flush keep-alive handles:

process.on('SIGTERM', () => {
  pooledAgent.destroy();
  cacheAgent.destroy();
  process.exit(0);
});

Step 7: Route Requests Through Proxies

Needle supports HTTP and HTTPS proxies with optional authentication:

await needle('get', 'https://api.example.com/data', {
  proxy: 'http://proxy.example.com:8080'
});

Add credentials with standard URL format:

await needle('get', 'https://api.example.com/data', {
  proxy: 'http://username:password@proxy.example.com:8080'
});

Proxy Rotation for Web Scraping

Rotating proxies prevents IP blocks during large scraping jobs. Here's a simple rotation pattern:

const proxies = [
  'http://user:pass@proxy1.example.com:8080',
  'http://user:pass@proxy2.example.com:8080',
  'http://user:pass@proxy3.example.com:8080'
];

let proxyIndex = 0;

function getNextProxy() {
  const proxy = proxies[proxyIndex];
  proxyIndex = (proxyIndex + 1) % proxies.length;
  return proxy;
}

async function scrapeWithRotation(url) {
  return needle('get', url, {
    proxy: getNextProxy(),
    headers: {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
  });
}

For production scraping, consider residential proxies from providers like Roundproxies.com. They offer residential, datacenter, ISP, and mobile proxies that blend in with normal traffic patterns.

Step 8: Handle Authentication Like a Pro

Needle supports Basic, Digest, and custom authentication methods.

Basic Authentication

await needle('get', 'https://api.example.com/private', {
  username: 'alice',
  password: 'secretpassword'
});

Digest Authentication

await needle('get', 'https://api.example.com/secure', {
  username: 'alice',
  password: 'secretpassword',
  auth: 'digest'
});

Auto-Detect Authentication Type

await needle('get', 'https://api.example.com/data', {
  username: 'alice',
  password: 'secretpassword',
  auth: 'auto'
});

The auto option checks server response headers and uses the appropriate method.

Bearer Token Authentication

For OAuth2 and JWT, set the header directly:

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';

await needle('get', 'https://api.example.com/user', {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

Step 9: Set Global Defaults for Cleaner Code

Repeating options on every request clutters your code. Set defaults once:

needle.defaults({
  timeout: 30000,
  user_agent: 'MyApp/2.0',
  follow_max: 5,
  json: true
});

Now every request inherits these settings unless you override them.

Pro tip: Create specialized instances for different APIs:

async function internalApi(method, endpoint, data = null) {
  return needle(method, `https://internal-api.company.com${endpoint}`, data, {
    json: true,
    headers: { 'X-Internal-Token': process.env.INTERNAL_TOKEN }
  });
}

async function externalApi(method, endpoint, data = null) {
  return needle(method, `https://external-api.example.com${endpoint}`, data, {
    json: true,
    timeout: 60000  // External APIs are slower
  });
}

Step 10: Handle Cookies and Sessions

Needle parses cookies automatically when parse_cookies is enabled (default: true):

const response = await needle('get', 'https://example.com/login');
console.log(response.cookies); // { session_id: 'abc123', user_pref: 'dark' }

Send cookies with subsequent requests:

await needle('get', 'https://example.com/dashboard', {
  cookies: { session_id: 'abc123' }
});

Build a Simple Session Manager

class SessionManager {
  constructor() {
    this.cookies = {};
  }

  async request(method, url, data = null, options = {}) {
    const response = await needle(method, url, data, {
      ...options,
      cookies: this.cookies
    });

    // Merge new cookies
    if (response.cookies) {
      Object.assign(this.cookies, response.cookies);
    }

    return response;
  }
}

const session = new SessionManager();
await session.request('post', 'https://example.com/login', { user: 'admin', pass: 'secret' });
await session.request('get', 'https://example.com/dashboard'); // Uses login cookies

Step 11: Debug and Troubleshoot Requests

Enable debug logging with the NODE_DEBUG environment variable:

NODE_DEBUG=needle node your-script.js

This prints every request and response, including headers and timing.

Check Response Details

const response = await needle('get', 'https://api.example.com/data');

console.log('Status:', response.statusCode);
console.log('Headers:', response.headers);
console.log('Bytes:', response.bytes);
console.log('Content-Type:', response.headers['content-type']);

Common Pitfalls to Avoid

Missing status checks. Needle doesn't throw on HTTP 500:

const response = await needle('get', 'https://api.example.com/data');

if (response.statusCode >= 400) {
  throw new Error(`HTTP ${response.statusCode}: ${response.body.error || 'Unknown error'}`);
}

Forgetting error handlers on streams:

const stream = needle.get('https://example.com/file.zip');

stream.on('error', (error) => {
  console.error('Stream failed:', error.message);
  stream.destroy();
});

stream.pipe(fs.createWriteStream('file.zip'));

Mixing promises and callbacks. Pick one style. Awaiting a call that also uses a callback means the callback never fires.

Needle vs Axios vs Got: When to Use What

Feature Needle Axios Got
Dependencies 2 5+ 10+
Bundle size ~50KB ~400KB ~500KB
Native streams Yes No Yes
Browser support No Yes No
Built-in retry No No Yes
Performance Fastest Moderate Moderate

Use Needle when:

  • You're building backend-only applications
  • Bundle size matters (serverless, edge functions)
  • You need streaming for large files
  • Performance is critical

Use Axios when:

  • You need browser compatibility
  • You want interceptors for global request/response handling
  • Your team is already familiar with it

Use Got when:

  • You need built-in retry and pagination
  • You're building CLI tools
  • You want hook-based middleware

Complete Example: Production API Client

Here's a complete, production-ready API client using Needle:

const needle = require('needle');
const https = require('https');

class ApiClient {
  constructor(baseUrl, options = {}) {
    this.baseUrl = baseUrl;
    this.timeout = options.timeout || 30000;
    this.maxRetries = options.maxRetries || 3;
    this.agent = new https.Agent({
      keepAlive: true,
      maxSockets: options.maxSockets || 25
    });
  }

  async request(method, endpoint, data = null, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    let lastError;

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const response = await needle(method, url, data, {
          json: true,
          agent: this.agent,
          signal: AbortSignal.timeout(this.timeout),
          ...options
        });

        if (response.statusCode >= 200 && response.statusCode < 300) {
          return response.body;
        }

        if (response.statusCode >= 400 && response.statusCode < 500) {
          throw new Error(`Client error ${response.statusCode}: ${JSON.stringify(response.body)}`);
        }

        lastError = new Error(`Server error: ${response.statusCode}`);
      } catch (error) {
        lastError = error;
      }

      if (attempt < this.maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
        await new Promise(r => setTimeout(r, delay));
      }
    }

    throw lastError;
  }

  get(endpoint, options) {
    return this.request('get', endpoint, null, options);
  }

  post(endpoint, data, options) {
    return this.request('post', endpoint, data, options);
  }

  put(endpoint, data, options) {
    return this.request('put', endpoint, data, options);
  }

  delete(endpoint, options) {
    return this.request('delete', endpoint, null, options);
  }

  destroy() {
    this.agent.destroy();
  }
}

// Usage
const api = new ApiClient('https://api.example.com', {
  timeout: 15000,
  maxRetries: 3
});

const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John', email: 'john@example.com' });

// Clean shutdown
process.on('SIGTERM', () => api.destroy());

Final Thoughts

Needle proves you don't need a heavyweight client for production-grade HTTP requests. You get streaming, automatic parsing, compression, proxy support, and connection pooling—all in a package that won't bloat your bundles.

The key takeaways:

  1. Use json: true for automatic serialization and parsing
  2. Add connection pooling with custom agents for high-throughput workloads
  3. Build retry logic with exponential backoff and jitter
  4. Stream large files to keep memory usage flat
  5. Set timeouts aggressively—10 seconds is usually plenty

Ready to switch from Axios or node-fetch? Start with one endpoint, measure the difference, and expand from there. Your cold starts and memory graphs will thank you.

FAQ

Is Needle still maintained in 2026?

Yes. Needle receives regular updates and has a stable API that hasn't changed significantly, which means your code won't break with new versions.

Can I use Needle in the browser?

No. Needle is Node.js only. It uses Node-specific modules like http, https, and stream. For browser applications, use Axios or the native Fetch API.

How does Needle compare to native fetch in Node.js 21+?

Native fetch is simpler for basic requests, but Needle offers more features: automatic JSON/XML parsing, built-in proxy support, cookie handling, and connection pooling. Needle is also faster in benchmarks due to its minimal overhead.

Does Needle support HTTP/2?

No. Needle uses the standard http and https modules, which default to HTTP/1.1. For HTTP/2, consider the got library or Node's native http2 module.