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:
- Serializes your object to a JSON string
- Sets the
Content-Typeheader toapplication/json - 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:
- Use
json: truefor automatic serialization and parsing - Add connection pooling with custom agents for high-throughput workloads
- Build retry logic with exponential backoff and jitter
- Stream large files to keep memory usage flat
- 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.