http.createServer()
Creates an http.Server that emits a request event for every incoming HTTP request.
http.createServer([options][, requestListener])- Added in Node.js v0.1.13
Parameters
| Parameter | Type | Description |
|---|---|---|
$optionsoptional | Object | Server tuning and class overrides. Read-only once the server is constructed. Notable keys: IncomingMessage / ServerResponse (subclasses to instantiate per request), insecureHTTPParser (default false), maxHeaderSize (default 16384), noDelay (default true), keepAliveTimeout (default 5000), requestTimeout (default 300000), headersTimeout (default 60000), joinDuplicateHeaders (default false), highWaterMark (default 16384), shouldUpgradeCallback. |
$requestListeneroptional | Function | Added automatically as a listener for the 'request' event. Receives (request, response). |
Return value
<http.Server>A new http.Server instance. It is not listening yet — call server.listen() to bind it.
Errors
createServer() itself does not throw for a well-formed call; bind failures such as EADDRINUSE arrive later as an 'error' event on the server, and unparsable requests as 'clientError'.
Changelog
- PHP v24.11.0, v22.23.0Added the shouldUpgradeCallback option, letting the server reject Upgrade requests it does not want.
- PHP v18.0.0The options object is now read-only — mutating it after construction has no effect.
- PHP v13.3.0The options argument became optional, so createServer(fn) is the documented short form.
http.createServer() builds the server object but does not open a port. The returned http.Server is inert until server.listen() is called, which is why the two almost always appear together.
When to reach for it
Reach for it when you want an HTTP endpoint with no framework in the dependency tree: a health check, a webhook receiver, a static file server, or the origin behind a reverse proxy. Frameworks such as Express and Koa are thin layers over this exact call — app.listen() creates one of these servers internally.
What trips people up
- Every request must be ended. If a handler returns without calling
response.end(), the socket stays open untilrequestTimeout(5 minutes by default) fires. Forgetting areturnafterend()is the other half of this bug: the handler falls through and writes twice. - The request body is a stream.
request.bodydoes not exist. You must consume'data'chunks yourself, or pipe the request somewhere. - Throwing inside the handler is fatal. An uncaught synchronous throw, or a rejected promise in an
asynchandler, takes down the process rather than returning 500. - The options object is read-only since v18. Change
server.keepAliveTimeouton the instance instead.
Example
const http = require('node:http');
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`${res.statusCode} ${http.STATUS_CODES[res.statusCode]}\n`);
});
// Malformed requests never reach the request listener.
server.on('clientError', (err, socket) => {
console.log('clientError:', err.code);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(3000, '127.0.0.1', () => {
console.log('listening on port', server.address().port);
});
// listening on port 3000When to use it
- Expose a /health endpoint for a Kubernetes liveness probe without pulling in a framework
- Receive Stripe or GitHub webhooks in a small standalone process
- Stand up a throwaway origin server in an integration test and tear it down after
- Serve a built static site from disk behind nginx
Frequently asked questions
Do I need Express to build a server in Node?
No — http.createServer() is a complete HTTP server, and Express is a routing and middleware layer built on top of it. Reach for a framework when you want path parameters, body parsing, and middleware composition; the built-in module is enough for a health check, a webhook receiver, or a single-purpose service.
Why does my Node server hang and never respond?
Almost always because response.end() was never called on some code path. Node keeps the socket open until requestTimeout (300000 ms by default) expires, so the client sees a hang rather than an error. Trace every branch of the handler, including early returns and catch blocks, and confirm each one ends the response.
What is the difference between http.createServer() and new http.Server()?
Nothing functional — http.createServer() is a factory that calls new http.Server() with the same arguments and returns the instance. Use createServer(); the constructor form exists mainly for subclassing.
More examples
Reading a JSON request body
Shows the whole body-parsing job that a framework normally hides: buffer the chunks, cap the total size so a client cannot exhaust memory, and handle a parse failure as a 400 rather than a crash. The console line prints sorted keys so the log is stable regardless of JSON key order.
const http = require('node:http');
const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(405, { Allow: 'POST' }).end();
return;
}
const chunks = [];
let size = 0;
req.on('data', (chunk) => {
size += chunk.length;
if (size > 1_000_000) {
res.writeHead(413).end();
req.destroy();
return;
}
chunks.push(chunk);
});
req.on('end', () => {
let body;
try {
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('invalid JSON');
return;
}
console.log('received keys:', Object.keys(body).sort().join(','));
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: true }));
});
});
server.listen(3000);Tuning timeouts and swapping in a subclass
The IncomingMessage option makes the server construct your subclass for every request, which is how per-request timing or context is attached without a WeakMap. The timeout values are the standard fix for random 502s behind AWS ALB or nginx: the proxy must give up on an idle connection before Node does, so keepAliveTimeout is raised above the proxy's own idle timeout.
const http = require('node:http');
class TimedRequest extends http.IncomingMessage {
constructor(socket) {
super(socket);
this.startedAt = process.hrtime.bigint();
}
}
const server = http.createServer({
IncomingMessage: TimedRequest,
keepAliveTimeout: 72_000, // longer than a typical ALB idle timeout
headersTimeout: 80_000, // must exceed keepAliveTimeout
requestTimeout: 30_000,
}, (req, res) => {
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - req.startedAt) / 1e6;
console.log(`${req.method} ${req.url} ${res.statusCode} in ${ms.toFixed(1)}ms`);
});
res.writeHead(204).end();
});
server.listen(3000);
Discussion