সরাসরি পর্বে যান
পর্ব ২৪ · ৯৩-এর মধ্যে · মডিউল ৪

Node.js server — http module, event loop আর async I/O

Node.js Server — the http Module, the Event Loop and Async I/O

Taught in Bangla, this lesson builds a server with node:http: req and res, reading the request body as a stream with limits and Bangla text, routing by hand and where it starts to hurt, what one turn of the event loop does, non-blocking I/O and the libuv thread pool, a measured example of blocking the loop, concurrency versus parallelism, and a framework-free Bazarghar products API to compare with Express in the next lesson.

এই পর্বে যা শিখবেন

  • শুধু node:http দিয়ে JSON API লেখা, status আর header ঠিকভাবে পাঠানো
  • Request body-কে stream হিসেবে পড়া, সীমা বসানো আর বাংলা লেখা না ভাঙা
  • Event loop-এর একটি ঘুরে কোন কাজ কখন চলে, সেটি অনুমান করা
  • কোন কাজ OS-এ যায়, কোনটি thread pool-এ, আর কোনটি loop আটকে দেয়, মেপে দেখা
  • Concurrency আর parallelism আলাদা করা, আর worker_threads কখন লাগে বোঝা
  • ES module, node --watch দিয়ে কাজের ছোট একটি backend গোছানো

১ · Server-এ JavaScript চালানো মানে কী

ধরুন “বাজারঘর”-এর React পাতা থেকে একজন ক্রেতা পণ্যের তালিকা চাইলেন। পর্ব ১৯-এ TanStack Query দিয়ে সেই ডেটা আনার দিকটি দেখেছেন। কিন্তু উত্তরটি দিচ্ছে কে? দূরের কোনো কম্পিউটারে একটি প্রোগ্রাম চলছে, যে একটি নির্দিষ্ট দরজায় কান পেতে বসে আছে। এই পর্বে আমরা সেই প্রোগ্রামটি নিজের হাতে লিখব।

Browser-এর JavaScript চলে ব্যবহারকারীর ফোনে, DOM আর বোতামের মাঝে। Node.js সেই একই ভাষাকে বের করে আনে server-এ। সেখানে DOM নেই, window নেই; আছে ফাইল, network আর process. Node-এর ভেতরে দুটি বড় অংশ: V8, যা JavaScript চালায়, আর libuv, যা অপেক্ষার কাজ সামলায়। Node-এর মূল API-র পরিচয় পাবেন JavaScript কোর্সের Node.js পর্বে; এখানে আমরা শুধু server-এর দিকে তাকাব।

একটি ছোট রেস্তোরাঁ ভাবুন। ওয়েটার একজনই। তিনি অর্ডার নিয়ে রান্নাঘরে দিয়ে আসেন, ভাত সেদ্ধ হওয়া পর্যন্ত চুলার পাশে দাঁড়িয়ে থাকেন না। সেই ফাঁকে পরের টেবিলে যান। রান্না শেষ হলে ঘণ্টা বাজে, আর তিনি খাবার পৌঁছে দেন। এভাবে একজন মানুষই বিশটি টেবিল সামলাতে পারেন, যতক্ষণ প্রতিটি টেবিলের কাজের বেশিরভাগটাই “অপেক্ষা”। কিন্তু কোনো টেবিল যদি তাঁকে দিয়েই পনেরো মিনিট ধরে পেঁয়াজ কাটায়, তখন বাকি উনিশটি টেবিল বসে থাকে। Node.js ঠিক এই ওয়েটার।

Node.js-এর ভেতরে কে কোন কাজ করে

One JavaScript thread, with waiting handed to the OS and to a small thread pool

Node.js-এর স্তর: JavaScript, event loop, OS আর thread pool আপনার JavaScript Event loop (libuv) OS kernel Thread pool (৪টি) Network Disk, CPU V8 · একটি main thread কোন কাজ তৈরি, কার callback এখন চলবে socket-এর খবর epoll, kqueue, IOCP ফাইল, dns.lookup crypto, zlib
ছবি ২৪.১ — JavaScript নিজে অপেক্ষা করে না। Network-এর অপেক্ষা OS করে, ফাইলের মতো কাজ thread pool করে, আর event loop খবর এনে দেয়।

২০০৯ সালে Ryan Dahl Node.js তৈরি করেন, আর সেই বছর JSConf EU-তে সবার সামনে দেখান। তখন প্রচলিত ধারা ছিল প্রতিটি সংযোগে একটি thread বা process. তাঁর যুক্তি ছিল, একটি server-এর বেশিরভাগ সময় কাটে disk আর network-এর অপেক্ষায়, তাই অপেক্ষার জন্য thread আটকে রাখা অপচয়। V8 তখন নতুন আর দ্রুত, আর JavaScript-এ আগে থেকে কোনো blocking I/O library-র অভ্যাস ছিল না। তাই সব I/O-কে শুরু থেকেই async করা সহজ হয়েছিল।

২ · প্রথম server — node:http, req আর res

কোনো npm package ছাড়াই শুরু করা যায়, কারণ node:http Node-এর সাথেই আসে। মূল ভাবনা একটিই: http.createServer(fn) একটি server বানায়, আর প্রতিটি request-এ fn(req, res) ডাকা হয়। server.listen(port) তাকে একটি port-এ বসায়। Port হলো একই কম্পিউটারের নম্বর দেওয়া দরজা। আপনি যে function দিলেন, সেটি একবারই লেখা হয়, কিন্তু চলে প্রতিটি request-এ।

Objectঅংশকী দেয় বা করে
reqreq.methodHTTP method: "GET", "POST" ইত্যাদি
reqreq.urlpath আর query একসাথে, যেমন /api/products?category=rice. Domain থাকে না।
reqreq.headersসব header, নাম ছোট হাতের অক্ষরে
reqনিজেই streamBody আসে টুকরো টুকরো। req.body বলে কিছু নেই।
resres.statusCode, res.setHeader()Status আর header ঠিক করা
resres.writeHead()Status আর header একবারে ঠিক করা
resres.end(body)Body পাঠিয়ে উত্তর শেষ করা। না ডাকলে client অপেক্ষাই করতে থাকে।

নিচের ব্লকে একটি সত্যিকারের server চলছে, আর একই ব্লকে fetch তাকে ডাকছে। Port 0 মানে OS একটি খালি port দেবে। fetch-এর ব্যাখ্যা আছে JavaScript-এর fetch পর্বে; HTTP বার্তার গঠন দেখেছেন পর্ব ০২-এ।

const http = require("node:http");
const products = [
  { id: 1, name: "Miniket Rice 5kg", price: 420 },
  { id: 2, name: "Masur Dal 1kg", price: 160 },
];

function sendJson(res, status, data) {
  const body = JSON.stringify(data);
  res.statusCode = status;
  res.setHeader("Content-Type", "application/json; charset=utf-8");
  res.setHeader("Content-Length", Buffer.byteLength(body));
  res.setHeader("Connection", "close");
  res.end(body);
}

const server = http.createServer((req, res) => {
  console.log("server saw:", req.method, req.url);
  const { pathname } = new URL(req.url, "http://localhost");
  const m = pathname.match(/^\/api\/products\/(\d+)$/);
  if (req.method === "GET" && pathname === "/api/products") {
    return sendJson(res, 200, products);
  }
  const item = m && products.find((p) => p.id === Number(m[1]));
  if (req.method === "GET" && item) return sendJson(res, 200, item);
  sendJson(res, 404, { error: "not found" });
});

server.listen(0, "127.0.0.1", async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  for (const path of ["/api/products", "/api/products/2", "/api/products/9"]) {
    const r = await fetch(base + path);
    console.log(r.status, r.headers.get("content-length"), await r.text());
  }
  server.close();
});

প্রতিটি request-এ server saw: লাইন আসে, তারপর client-এর লাইন। দ্বিতীয় উত্তর হলো 200 43 {"id":2,"name":"Masur Dal 1kg","price":160}, অর্থাৎ status 200 আর body 43 bytes. তৃতীয়টি 404 21 {"error":"not found"}. কেউ আমাদের হয়ে 404 দেয়নি; শেষ লাইনটি নিজে না লিখলে উত্তরই যেত না। Buffer.byteLength ব্যবহার করা হয়েছে কারণ Content-Length গোনে bytes, অক্ষর নয়। বাংলা অক্ষর সাধারণত ৩ bytes নেয়, তাই body.length এখানে ভুল উত্তর দিত। Connection: close শুধু এই ডেমো দ্রুত শেষ করার জন্য; কারণটি পরের অংশে।

একটি request ভেতরে কীভাবে এগোয়, সেটি ধাপে ধাপে দেখুন। উদাহরণটি উপরের ব্লকের GET /api/products/2.

  1. Clientfetch()bytes পাঠায়
  2. OS kernelsocketbytes ধরে রাখে
  3. Event looppollখবর পায়
  4. HTTP parserreqbytes থেকে object
  5. আপনার function(req, res)উত্তর লেখে
তারে যা গেলশুধু লেখা, কোনো object নয়
GET /api/products/2 HTTP/1.1
host: 127.0.0.1:PORT
accept: */*
… আরও header
KernelNode এখনো কিছু জানে না
TCP connection গৃহীত
socket buffer-এ bytes জমা
আপনার JavaScript চলছে না, অপেক্ষাও করছে না
libuvpoll phase
OS: “এই socket-এ পড়ার মতো ডেটা আছে”
libuv: socket-এর callback চালানোর পালা
JavaScript ব্যস্ত থাকলে এই ধাপই পিছিয়ে যায়
reqheader পড়া শেষ, body বাকি থাকতে পারে
req.method  = "GET"
req.url     = "/api/products/2"
req.headers = { host: "...", accept: "*/*" }
resএকই পথে ফেরা
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 43
Connection: close
Date: …

{"id":2,"name":"Masur Dal 1kg","price":160}
  1. Client পাঠাল। fetch একটি TCP সংযোগ খুলে HTTP বার্তা লিখল। তারে যাচ্ছে শুধু লেখা।
  2. Kernel ধরল। Bytes প্রথমে OS-এর কাছে পৌঁছায়, socket-এর buffer-এ জমে। Node ব্যস্ত থাকলেও সংযোগ বাতিল হয় না; bytes এখানে অপেক্ষা করে।
  3. Event loop খবর পেল। Loop-এর poll phase-এ OS জানায় কোন socket-এ ডেটা এসেছে। JavaScript অন্য কাজে আটকে থাকলে এই মুহূর্তটিই দেরি হয়।
  4. Parser কাজ করল। Node-এর HTTP parser লেখা থেকে req বানায়। লক্ষ করুন, এখানে header পড়া হয়েছে; body আসবে পরে, stream হয়ে।
  5. আপনার function চলল। sendJson status আর header ঠিক করে res.end ডাকল। Bytes আবার kernel হয়ে client-এ গেল। এই function-এর সময়টুকুই আপনার হাতে; বাকিটা Node আর OS-এর।

Status আর header: কোনটি কার দায়িত্ব

Status code client-কে এক সংখ্যায় ফল জানায়। এই পর্বের API-তে আমরা ব্যবহার করব 200 (ঠিক আছে), 201 (তৈরি হলো), 400 (অনুরোধ ভুল), 404 (পাওয়া যায়নি), 405 (এই method চলবে না), 413 (body খুব বড়), 415 (ভুল ধরনের body), 422 (গঠন ঠিক, মান ভুল) আর 500 (আমাদের দোষ)। এদের নকশার যুক্তি আসবে পর্ব ৩৩-এ। Node নিজে থেকে শুধু 200 ধরে নেয়; ঠিক সংখ্যাটি বসানো আপনার কাজ।

Header-এর ক্ষেত্রে Node কিছু কাজ নিজেই করে। আপনি না দিলেও Date যোগ হয়, আর keep-alive চালু থাকে, যাতে একই TCP সংযোগে পরের request আসতে পারে। অংশ ৯-এর বাস্তব run-এ Keep-Alive: timeout=5 দেখবেন। Keep-alive-এর কারণেই শেষ request-এর পরেও সংযোগ কিছুক্ষণ খোলা থাকে। পাতার ব্লকে তাই Connection: close বসিয়েছি, যাতে server তাড়াতাড়ি বন্ধ হয়। কাজের API-তে এটি বসাবেন না, কারণ প্রতিবার নতুন সংযোগ খোলা ধীর।

৩ · Request body একটি stream — টুকরো জোড়া, সীমা আর বাংলা

Header ছোট, তাই একবারেই আসে। কিন্তু body ১০ MB-র ছবিও হতে পারে। সেটি পুরোটা memory-তে তুলে তারপর আপনাকে দিলে একশো জন ব্যবহারকারীই server-এর memory শেষ করে দিতে পারেন। তাই Node body দেয় stream হিসেবে: req নিজেই একটি Readable stream, যার ডেটা আসে Buffer-এর টুকরো হয়ে। Express-এ যে req.body পাবেন, সেটি কেউ এই টুকরোগুলো জোড়া দিয়ে বানিয়েছে। পর্ব ২৬-এ দেখবেন সেই কাজ করে একটি middleware.

সরকারি অফিসে একটি মোটা ফাইল এক টানে আসে না। পিয়ন কয়েক দফায় কাগজ দিয়ে যান। কর্মকর্তা প্রতিটি দফা গুনে রাখেন, আর একশো পাতার বেশি হলেই বলে দেন “এত নেওয়া যাবে না”। সব কাগজ এলে তবেই ফাইল সেলাই করেন। Stream পড়াও তাই: গোনা, সীমা দেখা, শেষে জোড়া।

const http = require("node:http");
const LIMIT = 200;                       // সর্বোচ্চ bytes

async function readJson(req) {
  const parts = []; let size = 0, chunks = 0;
  for await (const chunk of req) {       // req একটি readable stream
    size += chunk.length; chunks += 1;
    if (size > LIMIT) throw Object.assign(new Error("too big"), { status: 413 });
    parts.push(chunk);
  }
  console.log(`  body: ${chunks} chunk, ${size} bytes`);
  return JSON.parse(Buffer.concat(parts).toString("utf8"));
}
function send(res, status, data) {
  res.writeHead(status, { "Content-Type": "application/json", Connection: "close" });
  res.end(JSON.stringify(data));
}
const server = http.createServer(async (req, res) => {
  try {
    const p = await readJson(req);
    send(res, 201, { id: 2, name: p.name, price: p.price });
  } catch (e) {
    if (e instanceof SyntaxError) return send(res, 400, { error: "invalid JSON" });
    send(res, e.status ?? 500, { error: e.status ? e.message : "server error" });
  }
});

server.listen(0, "127.0.0.1", async () => {
  const url = `http://127.0.0.1:${server.address().port}/api/products`;
  for (const body of ['{"name":"Soybean Oil 1L","price":190}', "{bad json", "x".repeat(300)]) {
    const r = await fetch(url, { method: "POST", body });
    console.log(r.status, await r.text());
  }
  server.close();
});

প্রথম POST-এ body: 1 chunk, 37 bytes, তারপর 201 {"id":2,"name":"Soybean Oil 1L","price":190}. ছোট body-তে একটি টুকরোই আসে, কিন্তু সেটি নিশ্চিত নয়। দ্বিতীয়টিতে 400 {"error":"invalid JSON"}. তৃতীয়টি সীমা পেরোতেই 413 {"error":"too big"}; এখানে body লাইনটি ছাপাই হয়নি, কারণ পুরোটা পড়ার আগেই থেমে গেছে। SyntaxError আলাদা করে ধরা হয়েছে, আর অজানা ভুলে শুধু server error বলা হচ্ছে, যাতে ভেতরের তথ্য বাইরে না যায়। মান যাচাই এখানে নেই: name না থাকলেও item তৈরি হবে। সেই কাজ পর্ব ২৭-এর।

বাংলা ভাঙার ফাঁদ: অনেক টিউটোরিয়াল আর AI লেখে body += chunk. এটি প্রতিটি টুকরোকে আলাদাভাবে লেখায় বদলায়। একটি বাংলা অক্ষরের ৩ bytes দুই টুকরোয় পড়লে অক্ষরটি নষ্ট হয়। সত্যিকারের run-এ “চাল” শব্দটির ৯ bytes-কে ৪ আর ৫ bytes-এ ভেঙে পাঠিয়ে পাওয়া গেল:

body += chunk : চ���ল
Buffer.concat : চাল

তাই আগে সব Buffer জমান, শেষে একবারে Buffer.concat(parts).toString("utf8"). ইংরেজি ডেটায় এই বাগ কখনো দেখা যায় না, তাই ধরা পড়ে দেরিতে।

সীমা কেন ঐচ্ছিক নয়? Body-র সীমা না থাকলে একজন আক্রমণকারী কয়েক GB পাঠিয়ে memory শেষ করতে পারেন। Node এর কিছু অংশ নিজে সামলায়: header-এর মোট সীমা ডিফল্টে 16 KiB, পুরো request-এর সময়সীমা ডিফল্টে ৩০০ সেকেন্ড। কিন্তু body-র আকারের কোনো ডিফল্ট সীমা node:http-এ নেই। নিরাপত্তার বিস্তার আছে API security পর্বে। একটি বড় ফাইল জমা নেওয়ার কৌশল আসবে পর্ব ৩০-এ।

৪ · হাতে routing — কোথায় ব্যথা শুরু হয়

ওপরের প্রথম ব্লকেই আপনি একটি router লিখেছেন: কয়েকটি if, যা method আর path দেখে ঠিক করে কোন কোড চলবে। দুটি route-এ এটি চমৎকার। কিন্তু “বাজারঘর”-এ শিগগিরই পণ্য, অর্ডার, কার্ট, ব্যবহারকারী, রিভিউ মিলিয়ে ৪০টি route হবে। তখন প্রতিটি নতুন route-এর সাথে নিচের কাজগুলো হাতে করতে হয়:

কাজহাতে করলেকোন পর্বে সমাধান
/api/products/:id থেকে id আলাদা করাপ্রতিটিতে একটি regexপর্ব ২৫
405 আর Allow headerপ্রায় সবাই ভুলে যানপর্ব ২৫, ৩৩
JSON body পড়া, সীমা বসানোপ্রতিটি POST-এ ডাকতে হয়পর্ব ২৬
Login যাচাই, log, CORSপ্রতিটি handler-এ একই লাইনপর্ব ২৬
ইনপুট যাচাইলম্বা if-এর সারিপর্ব ২৭
Async error ধরাএকটি ভুল হলে পুরো server পড়ে যেতে পারেপর্ব ২৮

শেষ সারিটি শুধু বিরক্তিকর নয়, বিপজ্জনকও। async handler-এর ভেতরে কোনো ভুল ধরা না পড়লে promise reject হয়, আর Node ডিফল্টে সেই অবস্থায় পুরো process বন্ধ করে দেয়। একটি খারাপ request-এ JSON.parse ব্যর্থ করিয়ে আমরা এটি মেপে দেখেছি:

$ node crash.js
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
Node.js v24.15.0
$ echo $?
1

সত্যিকারের run · Node 24.15.0 · stack trace বাদ দেওয়া

অর্থাৎ একজনের ভুল request-এ সবার সেবা বন্ধ। তাই ওপরের ব্লকে try/catch ছিল। Framework-এর আসল মূল্য এখানেই: Express কোনো জাদু নয়, এই পুনরাবৃত্তিগুলোর একটি ভালোভাবে পরীক্ষিত সমাধান। তবে আগে হাতে করে দেখা জরুরি, নইলে framework ভুল করলে কারণ খুঁজে পাবেন না। Express-এর সাথে এই একই কোডের তুলনা আছে JavaScript কোর্সের Express পর্বে, আর এই কোর্সে পর্ব ২৫-এ আমরা “বাজারঘর”-কে সেখানে নিয়ে যাব।

৫ · Event loop — একটি ঘুরে কী কী হয়

Request এলে কে আপনার function ডাকে? Event loop. এটি একটি সাধারণ লুপ, যা বারবার জিজ্ঞেস করে: “কোন কাজ তৈরি?” তারপর একে একে তাদের callback চালায়। মূল ধারণা, microtask আর ক্রমের ধাঁধা বিস্তারিত আছে JavaScript-এর event loop পর্বে; Promise আর async/await-এর ভাষা শিখতে দেখুন Promises আর async/await. এখানে শুধু একটি server-এর চোখে দেখি।

Phaseএখানে যা চলেServer-এ উদাহরণ
pollনতুন I/O-এর খবর আনা, তাদের callback চালানোনতুন request, database-এর উত্তর, ফাইল পড়া শেষ
checksetImmediateলম্বা কাজ টুকরো করে পরের অংশ
closeবন্ধ হওয়ার callbacksocket.on("close")
timersসময় হয়ে যাওয়া setTimeout, setIntervalRetry, timeout
pendingআগের ঘুরে পিছিয়ে যাওয়া কিছু I/O callbackকিছু TCP error

আর দুটি ছোট লাইন আছে, যা কোনো phase নয়। প্রতিটি callback শেষ হলে পরের callback-এর আগে Node খালি করে process.nextTick-এর queue, তারপর Promise-এর microtask queue. তাই await-এর পরের কোড খুব তাড়াতাড়ি চলে।

Timers কি আগে, না পরে? অনেক লেখায় timers phase সবার আগে দেখানো হয়। Node-এর নিজের পাতা বলছে, libuv 1.45.0 (Node 20) থেকে timers চলে প্রতিটি ঘুরে poll-এর পরে, আর loop ঢোকার আগে একবার। আগে poll-এর আগে চলত। আসলে এটি একটি চক্র, তাই “প্রথম” কথাটির বিশেষ দাম নেই। মনে রাখার মতো নিশ্চিত নিয়ম একটি: I/O callback-এর ভেতর থেকে ডাকলে setImmediate সবসময় setTimeout(fn, 0)-এর আগে চলে। মূল script থেকে ডাকলে এই ক্রম নিশ্চিত নয়।

একটি request handler হলো poll phase-এর callback, অর্থাৎ I/O-এর ভেতরেই। তাই নিচের ক্রমটি নিশ্চিত।

const http = require("node:http");
const log = (m) => console.log(m);

const server = http.createServer((req, res) => {
  log("1 handler starts (poll phase)");
  setTimeout(() => log("6 setTimeout 0"), 0);
  setImmediate(() => log("5 setImmediate (check phase)"));
  Promise.resolve().then(() => log("4 promise microtask"));
  process.nextTick(() => log("3 nextTick"));
  res.setHeader("Connection", "close");
  res.end("ok");
  log("2 handler ends");
});

server.listen(0, "127.0.0.1", async () => {
  const r = await fetch(`http://127.0.0.1:${server.address().port}/`);
  await r.text();
  server.close(() => log("7 server closed"));
});

আউটপুট সংখ্যার ক্রমে: 1 handler starts থেকে 7 server closed. res.end ডাকার পরেও 2 handler ends ছাপা হয়, কারণ res.end function-কে থামায় না। আগে সব sync লাইন, তারপর 3 nextTick, তারপর 4 promise microtask. Handler poll-এর ভেতরে চলছিল, তাই পরের স্টেশন check: 5 setImmediate. 6 setTimeout 0 আসে তারপরে, যদিও কোডে আগে লেখা। ধাপগুলো এবার ধীরে দেখুন।

Loop-এর অবস্থা
এখন
nextTick
Promise
setImmediate
timer
  1. Handler চলছে। Poll phase-এ request-এর callback চলল। 1 আর 2 ছাপা হলো। পথে চারটি কাজ চারটি আলাদা লাইনে গেল। লক্ষ করুন, লেখা হয়েছে timer আগে, কিন্তু সে লাইনের সবচেয়ে পিছনে।
  2. Callback শেষ → nextTick. Node প্রথমে nextTick-এর লাইন খালি করে: 3.
  3. তারপর promise. এবার microtask: 4. এই দুই লাইন খালি না হওয়া পর্যন্ত loop এগোয় না।
  4. check phase. Poll-এর ঠিক পরের স্টেশন check, তাই setImmediate: 5.
  5. close পেরিয়ে timers. সময় হয়ে যাওয়া timer চলল: 6. একটি ঘুর এখানেই শেষ।
  6. পরের ঘুর, poll. fetch-এর উত্তর পড়া শেষ হলে client server.close ডাকে। শেষ সংযোগ বন্ধ হলে callback চলল: 7. অপেক্ষার আর কিছু নেই, তাই process নিজে থেকে শেষ হলো।

শেষ ধাপটিই বলে দেয় server কেন চলতেই থাকে। Loop ততক্ষণ ঘোরে, যতক্ষণ কিছু একটির অপেক্ষা বাকি। একটি খোলা listening socket মানে “অপেক্ষা বাকি”। server.close() না ডাকলে পাতার ব্লক কখনো শেষ হতো না।

আরও গভীরে

Poll phase-এ libuv OS-এর কাছে একসাথে অনেক socket-এর খবর চায়: Linux-এ epoll, macOS-এ kqueue, Windows-এ IOCP. কাজ না থাকলে এখানে ঘুমায়, তাই খালি Node server CPU খরচ করে না। মাপার জন্য আছে perf_hooks.monitorEventLoopDelay(), যা loop কত দেরি করে ঘুরছে তার histogram দেয়। Production-এ loop delay একটি ভালো স্বাস্থ্য-সংকেত, যা পর্ব ৮৩-এর monitoring-এ কাজে লাগবে।

৬ · Non-blocking I/O আর thread pool — অপেক্ষা কে করে

“Node single-threaded” কথাটি অর্ধেক সত্য। আপনার JavaScript একটি thread-এ চলে, কিন্তু Node process-এ আরও thread থাকে। Non-blocking I/O মানে “কাজটি শুরু করে দিলাম, শেষ হলে খবর পাব”। কিন্তু সেই “জানানো” দুইভাবে হয়।

OS নিজে জানায়

Network socket. Kernel একাই হাজারো socket নজরে রাখতে পারে, কোনো অতিরিক্ত thread লাগে না। এজন্যই Node অনেক সংযোগ ধরে রাখতে পারে।

HTTP, database-এর TCP সংযোগ, dns.resolve

Thread pool-এ পাঠাতে হয়

যে কাজের জন্য সব OS-এ ভালো async উপায় নেই, বা যা CPU-ভারী। libuv-এর ৪টি thread এগুলো করে, ডিফল্টে।

fs, dns.lookup, crypto.pbkdf2, crypto.scrypt, zlib

Thread pool মাত্র ৪ জনের। পঞ্চম কাজটি লাইনে দাঁড়ায়। এটি মেপে দেখুন: ৮টি crypto.pbkdf2 একসাথে দেওয়া হলো, প্রতিটি 10 লাখ রাউন্ডের, আর প্রতিটি শেষ হওয়ার সময় (ms) ছাপা হলো।

$ UV_THREADPOOL_SIZE=4 node pool.js
123 125 198 234 339 356 371 407
$ UV_THREADPOOL_SIZE=8 node pool.js
133 144 148 185 215 216 249 274

সত্যিকারের run · Node 24.15.0 · একটি local machine-এ একবারের মাপ, অন্য কাজও চলছিল; সাধারণ ফল নয়

প্রথম সারিতে ৪টি কাজ শেষ হয় ২৩৪ ms-এর মধ্যে, বাকি ৪টি পিছনের দলে, ৪০৭ ms পর্যন্ত। থ্রেড বাড়িয়ে ৮ করলে সব শেষ হয় ২৭৪ ms-এ। আদর্শ হিসাবে একটি কাজ ১২০ ms ধরলে ৪ thread-এ দুই দফা, মানে ২৪০ ms হওয়ার কথা; বাস্তবে বেশি লেগেছে, কারণ CPU core অন্য কাজের সাথে ভাগ হয়। UV_THREADPOOL_SIZE বসাতে হয় process চালু করার সময়, কোডের ভেতর থেকে process.env-এ বসালে কাজ করবে, এমন নিশ্চয়তা Node দেয় না।

মূল ধারণা

একই নামের দুটি কাজ দুই পথে যায়। fetch বা database query সাধারণত OS-এর পথে, তাই সংযোগ বাড়লে thread লাগে না। কিন্তু fs.readFile বা bcrypt-জাতীয় hash থাকে thread pool-এর ৪ জনের লাইনে। Login-এর ভিড়ে তাই ফাইল পড়াও ধীর হয়ে যেতে পারে, যদিও দুটির মধ্যে কোনো সম্পর্ক দেখা যায় না।

৭ · যা loop আটকে দেয় — আসল মাপ

এবার আসল বিপদ। Blocking call মানে JavaScript-এর thread নিজেই কাজ করছে বা অপেক্ষা করছে। সেই সময় loop ঘুরতে পারে না, তাই অন্য কোনো request-এর callback চলে না। মনে রাখবেন, একটি process-এর সব ব্যবহারকারী সেই একই thread ভাগ করছেন।

const http = require("node:http");
const { pbkdf2, pbkdf2Sync } = require("node:crypto");
const ROUNDS = 400_000;
const hash = (cb) => pbkdf2("pass", "salt", ROUNDS, 32, "sha256", cb);

const server = http.createServer((req, res) => {
  res.setHeader("Connection", "close");
  if (req.url === "/ping") return res.end("pong");
  if (req.url === "/sync") {
    pbkdf2Sync("pass", "salt", ROUNDS, 32, "sha256");   // loop আটকে
    return res.end("done");
  }
  hash(() => res.end("done"));                          // thread pool-এ
});

async function trial(base, path) {
  const t0 = performance.now();
  const ms = () => Math.round(performance.now() - t0);
  let slow, ping;
  const a = fetch(base + path).then((r) => r.text()).then(() => (slow = ms()));
  await new Promise((r) => setTimeout(r, 5));           // ৫ ms পরে দ্বিতীয় জন
  const b = fetch(base + "/ping").then((r) => r.text()).then(() => (ping = ms()));
  await Promise.all([a, b]);
  console.log(`${path.padEnd(6)} hash=${slow} ms, ping=${ping} ms`);
}

server.listen(0, "127.0.0.1", async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  await (await fetch(base + "/ping")).text();           // fetch গরম করা
  await trial(base, "/sync");
  await trial(base, "/async");
  server.close();
});

একটি run-এ পাওয়া গেল C:/Program Files/Git/sync hash=222 ms, ping=225 ms আর C:/Program Files/Git/async hash=205 ms, ping=10 ms. সংখ্যা machine ভেদে বদলাবে, কিন্তু ধরনটি একই। /sync-এ ping-এর উত্তর আসে hash শেষ হওয়ার পরে, অথচ ping-এর নিজের কাজ এক মিলিসেকেন্ডেরও কম। /async-এ hash-এ প্রায় সমান সময় লাগে, কিন্তু ping আগে ফেরে। কাজ কমেনি, শুধু thread বদলেছে। একটি সততার কথা: এই ব্লকে client আর server একই thread-এ, তাই /sync-এর সময় client-ও থেমে থাকে। আলাদা process থেকে মাপা ফল নিচের সারণিতে।

ঘড়ি ও অবস্থা
সময়
Main thread
Kernel-এ অপেক্ষায়
Thread pool
  1. A এল। Request A পাসওয়ার্ড hash চায়, আর handler pbkdf2Sync ডাকল। Main thread ২৩৭ ms-এর জন্য আটকে গেল। মাপা সংখ্যা নিচের সারণি থেকে; B আর C-র আসার সময় উদাহরণের জন্য ধরা।
  2. B এল। B শুধু /ping চায়। তার bytes kernel-এ পৌঁছাল, কিন্তু loop ঘুরছে না, তাই তাকে কেউ পড়ছে না।
  3. C এল। এবার দুজন অপেক্ষায়। ক্রেতা ভাবছেন সাইট ঝুলে গেছে, যদিও তাঁর কাজে কোনো সমস্যা নেই।
  4. A শেষ হলো। ২৩৭ ms-এ hash শেষ, A-র উত্তর গেল। Handler ফিরে আসতেই loop আবার ঘুরতে পারল।
  5. B আর C-র পালা। দুজনের কাজ এক মিলিসেকেন্ডেই শেষ, তবু B উত্তর পেলেন ২২৮ ms আর C ২১৮ ms দেরিতে। তাঁদের দোষ ছিল না।
  6. এবার async পথ। crypto.pbkdf2 কাজটি thread pool-এ পাঠিয়ে main thread-কে মুক্ত রাখে। B ১০ ms-এ এসে প্রায় সাথে সাথে উত্তর পেল।
  7. শেষ ফল। A এখন ২৪১ ms নিল, প্রায় একই। কিন্তু B আর C ১ ms-এ উত্তর পেলেন। Async কাজকে দ্রুত করে না; অন্যদের আটকায় না।

কোন কাজ কতটা আটকায়: মাপা সংখ্যা

আলাদা process থেকে client একটি ধীর request পাঠাল, আর সেটি চলার সময় প্রতি ১০ ms-এ /ping করল। খালি অবস্থায় ping-এর median ছিল ০.৯ ms. সবচেয়ে ধীর ping দেখায় loop কতটা আটকেছিল। প্রতিটি সারি ৫ বারের median.

Handler-এর কাজনিজের সময়সবচেয়ে ধীর pingফল
fs.readFileSync, 50 MB16 ms16 msআটকায়
fs.readFile, 50 MB22 ms1 msআটকায় না
JSON.stringify, 3 লাখ সারি47 ms44 msআটকায়
JSON.parse, 24 MB65 ms65 msআটকায়
CPU লুপ, 30 কোটি ঘুর841 ms836 msআটকায়
একই লুপ, Worker-এ1147 ms2 msআটকায় না
pbkdf2Sync, 20 লাখ রাউন্ড237 ms236 msআটকায়
pbkdf2 (async), 20 লাখ রাউন্ড241 ms1 msআটকায় না

সত্যিকারের run · Node 24.15.0 · একটি local machine, SSD, ফাইল OS cache-এ · সাধারণ benchmark নয়

তিনটি জিনিস লক্ষ করুন। প্রথমত, JSON.parse আর JSON.stringify async নয়; এদের async রূপও নেই। বড় তালিকা পাঠানো তাই নিজেই একটি blocking কাজ, আর সেজন্যই পর্ব ৩৪-এ pagination আসবে। দ্বিতীয়ত, readFileSync এখানে দ্রুত দেখাচ্ছে শুধু ফাইলটি cache-এ ছিল বলে। ধীর disk-এ সংখ্যাটি অনেক বড় হতে পারে। তৃতীয়ত, Worker-এ মোট সময় বেশি লেগেছে; নতুন Worker চালু করারও খরচ আছে। তবু ping প্রায় অক্ষত।

Throughput-এর হিসাব: ১০ ms কত বড়

একটি উত্তরের সময় নয়, পুরো server-এর ক্ষমতা মাপা যাক। autocannon 8.0.0 দিয়ে ৫০টি সংযোগ ৫ সেকেন্ড চাপ দিল, তিনটি আলাদা route-এ।

HandlerRequest/sp50 latencyp99 latency
শুধু JSON পাঠায়76,6140 ms1 ms
১০ ms অপেক্ষা (await)2,86115 ms35 ms
১০ ms sync CPU98.8506 ms528 ms

সত্যিকারের run · autocannon 8.0.0, Node 24.15.0 · একটি local machine, client একই কম্পিউটারে · একবারের মাপ

সংখ্যাগুলো হিসাব দিয়ে মিলিয়ে নেওয়া যায়। Latency আর throughput-এর সম্পর্ক এখানে স্পষ্ট:

  • Sync CPU: এক thread এক সেকেন্ডে 1000 ms ÷ 10 ms = ১০০টি কাজ করতে পারে। মাপা হয়েছে ৯৮.৮. একটি request-কে আগের ৫০ জনের লাইনে দাঁড়াতে হয়: 50 × 10 ms = 500 ms. মাপা p50 হলো 506 ms.
  • Async অপেক্ষা: ৫০ জন একসাথে অপেক্ষা করতে পারে। এই run-এ অপেক্ষা হয়েছে p50 15 ms, তাই 50 ÷ 0.015 s ≈ ৩,৩৩৩ request/s. মাপা ২,৮৬১, কারণ বাকি CPU খরচও আছে।
  • শিক্ষা: একই “১০ ms” অপেক্ষা হলে ক্ষমতা নামে ২৭ গুণ, CPU হলে প্রায় ৭৭৫ গুণ।
কাজের সূত্র

এক process-এর সর্বোচ্চ হার ≈ 1 ÷ (প্রতি request-এ main thread-এর CPU সময়)। অপেক্ষার সময় এই সূত্রে আসে না। আর Little’s Law বলে, একসাথে চলমান request L = λ × W, যেখানে λ হলো হার আর W হলো মোট সময়।

উদাহরণ: “বাজারঘর”-এর একটি handler-এ CPU 5 ms, database অপেক্ষা 40 ms. সর্বোচ্চ হার = 1 ÷ 0.005 = ২০০ request/s. সেই হারে চলমান request = 200 × 0.045 = ৯টি। ৪টি process চালালে প্রায় ৮০০ request/s পাওয়া যেতে পারে, যদি database সেই চাপ নিতে পারে। প্রায়ই পরের বাধা সেটিই।

আটকে গেলে কী করবেন

  • Async রূপ নিন: request-এর পথে readFileSync, pbkdf2Sync, execSync-এর মতো Sync নাম এড়িয়ে চলুন। Server চালুর সময় একবার config পড়তে sync ব্যবহার ঠিক আছে।
  • কম ডেটা: ৩ লাখ সারি না পাঠিয়ে pagination, আর বড় export হলে stream.
  • কাজ টুকরো করুন: লম্বা লুপকে ছোট ভাগে করে মাঝে setImmediate দিন, যাতে অন্যরা সুযোগ পায়।
  • সরিয়ে দিন: ছবি বানানো বা রিপোর্টের মতো ভারী কাজ worker_threads বা আলাদা background job-এ (পর্ব ৬২)। Regex-এও সাবধান: খারাপ regex একা loop আটকাতে পারে, যাকে বলে ReDoS.

৮ · Concurrency বনাম parallelism

দুটি শব্দ প্রায়ই গুলিয়ে যায়। Concurrency মানে একই সময়কালে অনেক কাজের দায়িত্ব নেওয়া, যদিও এক মুহূর্তে একটিই হচ্ছে। Parallelism মানে একই মুহূর্তে সত্যিই একাধিক কাজ চলা, যার জন্য একাধিক CPU core লাগে। চায়ের দোকানের একজন দোকানি চুলায় চা বসিয়ে বিস্কুট দেন, হিসাব লেখেন: এটি concurrency. দুজন দোকানি দুই চুলায় একসাথে চা বানান: এটি parallelism.

নিচের run-এ ২০টি request, প্রতিটিতে ১০০ ms-এর নকল database অপেক্ষা। Node-এর thread একটিই।

$ node conc.js
one by one : 2183 ms
all at once: 127 ms

সত্যিকারের run · Node 24.15.0 · একটি local machine-এ একবারের মাপ

২০ × 100 ms = ২ সেকেন্ড লাগত লাইনে দাঁড়ালে, কিন্তু একসাথে দিলে লাগল ১২৭ ms. এটি parallelism নয়, কারণ thread একটিই; অপেক্ষাগুলো একে অপরের ওপর পড়েছে। ঠিক এই কারণেই অপেক্ষার কাজে Node ভালো, আর ওপরের CPU কাজে নয়। এই মডেলের একটি সুবিধাও আছে: দুটি await-এর মাঝের কোড কেউ থামায় না, তাই একটি সাধারণ variable নিয়ে thread-জনিত গোলমাল হয় না। তবে await-এর দুই পাশে race condition ঠিকই হতে পারে। মডিউল ৬-এর পর্ব ৪৩-এ স্টকের হিসাবে সেটি দেখবেন।

কাজের ধরনউদাহরণউপায়
I/O-boundDatabase query, API callasync/await, এক thread-ই যথেষ্ট
CPU-bound, মাঝে মাঝেছবি ছোট করা, বড় হিসাবworker_threads
অনেক request, সব core ব্যবহার৪ core-এর VPSএকাধিক process (cluster বা load balancer)
ঘণ্টাখানেকের কাজমাসিক রিপোর্টBackground job (পর্ব ৬২)

worker_threads-এর সীমা: প্রতিটি worker-এর নিজের V8 instance আর event loop থাকে। ডেটা যায় বার্তা হিসেবে, কপি হয়ে; SharedArrayBuffer ছাড়া memory ভাগ হয় না। Node-এর ডকুমেন্টেশন স্পষ্ট বলে, worker CPU-ভারী JavaScript-এর জন্য, I/O-এর জন্য নয়, কারণ Node-এর নিজের async I/O বেশি দক্ষ। প্রতি request-এ নতুন worker না বানিয়ে একটি pool রাখা ভালো। Browser-এর Web Worker-এর সাথে ধারণায় মিল আছে: Web Workers দেখুন। আর thread-এর OS-স্তরের ব্যাখ্যা আছে Operating Systems-এর thread পর্বে.

৯ · “বাজারঘর”-এর products API — framework ছাড়া

এবার সব জোড়া দিই। আগে তিনটি ছোট প্রস্তুতি।

Version: পর্ব ০৫-এ দেখেছেন, কাজের project-এ LTS নিন, আর .nvmrc-এ version লিখে রাখুন। এখানে package.json-এর engines সেটিই জানায়। এই পর্বের project চলেছে Node 24.15.0-তে, আর পাতার ব্লকগুলো চলে Node 20 version-এ।

ES module: Node-এ দুটি module ব্যবস্থা আছে। পুরোনো CommonJS (require), যা পাতার ব্লকগুলোতে ব্যবহার হয়েছে, আর আধুনিক ES module (import). package.json-এ "type": "module" দিলে .js ফাইল ES module হয়। ভাষার নিয়ম আছে Modules পর্বে; Node-এ তিনটি পার্থক্য রোজ চোখে পড়ে:

যা লিখলেনNode যা বলল (সত্যিকারের run)সমাধান
import { a } from "./util"ERR_MODULE_NOT_FOUND… Did you mean to import "./util.js"?Extension লিখুন
require("node:fs")ReferenceError: require is not defined in ES module scopeimport fs from "node:fs"
__dirnameES module-এ এই নামটি নেইimport.meta.dirname

ES module-এর মূল স্তরে top-level await চলে, যা server চালুর আগে config পড়তে কাজে লাগে। একটি সূক্ষ্মতা: মূল স্তরে ক্রম উল্টে যায়। .mjs ফাইলে সত্যিকারের run-এ এসেছে sync promise nextTick, কারণ module-এর কোড নিজেই promise-এর কাজ হিসেবে চলে। Handler-এর ভেতরে আগের নিয়মই থাকে।

Watch mode: node --watch src/index.js ফাইল বদলালে process আবার চালু করে। আলাদা package লাগে না; v22.0.0 আর v20.13.0 থেকে এটি stable. এটি browser-এর HMR নয়: পুরো process নতুন করে শুরু হয়, তাই memory-তে থাকা ডেটা মুছে যায়। পর্ব ০৫-এর কাঠামোয় server/src/index.ts ছিল; এখানে Node-কে সরাসরি দেখার জন্য সাধারণ .js রাখা হয়েছে।

ফাইল চারটি: products.js (memory-তে ডেটা), http-utils.js, routes.js, index.js. পর্ব ০৪-এর স্তরের চিন্তা এখানে ছোট আকারে; পর্ব ২৯-এ সেটি পুরো গঠন পাবে। if-এর সারির বদলে route-এর একটি তালিকা রাখা হয়েছে, যাতে 405 আর Allow এক জায়গায় হয়।

import http from "node:http";
import { routes } from "./routes.js";
import { HttpError, sendJson } from "./http-utils.js";

const PORT = Number(process.env.PORT ?? 3000);

const server = http.createServer(async (req, res) => {
  const started = performance.now();
  const url = new URL(req.url, "http://localhost");
  try {
    const route = routes.find((r) => r.pattern.test(url.pathname));
    if (!route) throw new HttpError(404, "route not found");

    const handler = route.methods[req.method];
    if (!handler) {
      res.setHeader("Allow", Object.keys(route.methods).join(", "));
      throw new HttpError(405, "method not allowed");
    }
    const params = url.pathname.match(route.pattern).slice(1);
    await handler(req, res, { url, params });
  } catch (err) {
    if (!(err instanceof HttpError)) console.error(err);
    const status = err instanceof HttpError ? err.status : 500;
    sendJson(res, status, {
      error: status === 500 ? "internal server error" : err.message,
    });
  } finally {
    const ms = (performance.now() - started).toFixed(1);
    console.log(`${req.method} ${url.pathname} ${res.statusCode} ${ms}ms`);
  }
});

server.listen(PORT, "127.0.0.1", () => {
  console.log(`bazarghar API: http://127.0.0.1:${PORT}`);
});
import { addProduct, findProduct, listProducts } from "./products.js";
import { HttpError, readJson, sendJson } from "./http-utils.js";

function getList(req, res, ctx) {
  sendJson(res, 200, listProducts(ctx.url.searchParams.get("category")));
}

function getOne(req, res, ctx) {
  const product = findProduct(Number(ctx.params[0]));
  if (!product) throw new HttpError(404, "product not found");
  sendJson(res, 200, product);
}

async function create(req, res) {
  const { name, category, price } = await readJson(req);
  if (typeof name !== "string" || !name.trim() || !Number.isInteger(price) || price < 0) {
    throw new HttpError(422, "name and a non-negative integer price are required");
  }
  sendJson(res, 201, addProduct({ name: name.trim(), category, price }));
}

// হাতে লেখা route table: pattern, তারপর method অনুযায়ী handler
export const routes = [
  { pattern: /^\/api\/products$/, methods: { GET: getList, POST: create } },
  { pattern: /^\/api\/products\/(\d+)$/, methods: { GET: getOne } },
];
export class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

export function sendJson(res, status, data) {
  const body = JSON.stringify(data);
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(body),
  });
  res.end(body);
}

export async function readJson(req, limit = 10_000) {
  const type = req.headers["content-type"] ?? "";
  if (!type.startsWith("application/json")) {
    throw new HttpError(415, "Content-Type must be application/json");
  }
  const parts = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > limit) throw new HttpError(413, "body too large");
    parts.push(chunk);
  }
  try {
    return JSON.parse(Buffer.concat(parts).toString("utf8"));
  } catch {
    throw new HttpError(400, "invalid JSON");
  }
}
// সাময়িক ডেটা: memory-তে, server বন্ধ হলে মুছে যায়। Database আসবে মডিউল ৬-এ।
const products = [
  { id: 1, name: "মিনিকেট চাল ৫ কেজি", category: "rice", price: 420 },
  { id: 2, name: "মসুর ডাল ১ কেজি", category: "dal", price: 160 },
  { id: 3, name: "সয়াবিন তেল ১ লিটার", category: "oil", price: 190 },
];
let nextId = 4;

export function listProducts(category) {
  return category ? products.filter((p) => p.category === category) : products;
}

export function findProduct(id) {
  return products.find((p) => p.id === id);
}

export function addProduct({ name, category, price }) {
  const item = { id: nextId++, name, category, price };
  products.push(item);
  return item;
}
{
  "name": "bazarghar-server",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22" },
  "scripts": {
    "dev": "node --watch src/index.js",
    "start": "node src/index.js"
  }
}

এবার সত্যিকারের চালানো। এক terminal-এ server, অন্যটিতে curl.

$ cd ~/bazarghar/server
$ PORT=47833 node --watch src/index.js
bazarghar API: http://127.0.0.1:47833

$ B=http://127.0.0.1:47833/api/products

$ curl -i $B/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 96
Date: Sun, 01 Nov 2026 16:34:33 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":1,"name":"মিনিকেট চাল ৫ কেজি","category":"rice","price":420}

$ curl "$B?category=dal"
[{"id":2,"name":"মসুর ডাল ১ কেজি","category":"dal","price":160}]

$ curl -i -X POST $B -H "Content-Type: application/json" \
    -d '{"name":"Lal Chini 1kg","category":"sugar","price":150}'
HTTP/1.1 201 Created
…
{"id":4,"name":"Lal Chini 1kg","category":"sugar","price":150}

$ curl -i -X POST $B -H "Content-Type: application/json" -d '{bad json'
HTTP/1.1 400 Bad Request
…
{"error":"invalid JSON"}

$ curl -i -X POST $B -H "Content-Type: application/json" -d '{"name":"x","price":-5}'
HTTP/1.1 422 Unprocessable Entity
…
{"error":"name and a non-negative integer price are required"}

$ curl -X POST $B -d 'name=x'
{"error":"Content-Type must be application/json"}

$ curl -i -X DELETE $B/1
HTTP/1.1 405 Method Not Allowed
Allow: GET
…
{"error":"method not allowed"}

$ curl -i $B/999
HTTP/1.1 404 Not Found
…
{"error":"product not found"}

$ curl -i -X POST $B -H "Content-Type: application/json" --data-binary @big.json
HTTP/1.1 413 Payload Too Large
…
{"error":"body too large"}

সত্যিকারের run · Node 24.15.0, curl · পথ আর host নিরপেক্ষ করা হয়েছে · “…” মানে একই header বাদ

Server-এর terminal-এ নিজের log, আর একটি ফাইল বদলাতেই watch mode-এর কাজ:

GET /api/products/1 200 2.4ms
GET /api/products 200 0.5ms
POST /api/products 201 1.0ms
POST /api/products 400 0.4ms
POST /api/products 422 0.3ms
POST /api/products 415 0.2ms
DELETE /api/products/1 405 0.3ms
GET /api/products/999 404 0.2ms
GET /api/nothing 404 0.1ms
POST /api/products 413 0.6ms
Restarting 'src/index.js'
bazarghar API: http://127.0.0.1:47833

সত্যিকারের run · Node 24.15.0

প্রথম উত্তরে Content-Length: 96 দেখুন: JSON-এ অক্ষর কম, কিন্তু bytes বেশি, কারণ বাংলা। Restart-এর পরে নতুন পণ্যটি হারিয়ে যাবে; ডেটা memory-তে ছিল। Database আসবে মডিউল ৬-এ। আর একটি পরিচিত ভুল: একই port-এ দ্বিতীয় copy চালালে Node এভাবে থামে:

Error: listen EADDRINUSE: address already in use 127.0.0.1:47833
  …
  code: 'EADDRINUSE',

সত্যিকারের run · Node 24.15.0

আগের process বন্ধ করুন বা অন্য port দিন। API-টি চলছে, তবে এখন কত লাইন লেগেছে দেখুন, আর ভাবুন ৪০টি route, login আর যাচাই যোগ হলে কী হবে। এই চিন্তাটি নিয়েই পর্ব ২৫-এ যাবেন।

এই server শুধু 127.0.0.1-এ শোনে, তাই বাইরের কেউ পৌঁছাতে পারে না। শেখার সময় এটিই নিরাপদ। Production-এ সাধারণত একটি reverse proxy সামনে থাকে; বিস্তার পর্ব ৮২-তে।

পরীক্ষা ও ইন্টারভিউতে
  • Node single-threaded হলে এত request কীভাবে সামলায়? — JavaScript এক thread-এ, কিন্তু অপেক্ষা করে OS আর libuv; তাই thread আটকে থাকে না।
  • কোন কাজ thread pool-এ যায়? — fs, dns.lookup, pbkdf2, scrypt, zlib; network socket যায় না।
  • setImmediate বনাম process.nextTick? — nextTick চলমান callback-এর ঠিক পরে, promise-এরও আগে; setImmediate check phase-এ।
  • Concurrency আর parallelism-এর পার্থক্য? — প্রথমটি অনেক কাজ সামলানো, দ্বিতীয়টি একই মুহূর্তে অনেক কাজ করা।
  • Event loop আটকে গেছে, বুঝবেন কীভাবে? — ছোট route-ও ধীর হয়ে যায়, আর monitorEventLoopDelay-এর মান বাড়ে।

১০ · AI-এর যুগে — AI-এর লেখা Node server যাচাই

AI খুব সহজে “চলছে” এমন server লিখে দেয়। ঝামেলা হয় যা পরীক্ষায় ধরা পড়ে না, সেখানে: ছোট ইংরেজি ডেটায় সব ঠিক, ভিড়ে আর বাংলায় ভেঙে পড়ে। এই পর্বের মাপগুলোই আপনার যাচাইয়ের অস্ত্র।

AI যা লেখেফলযাচাই
Raw http-এ res.json(), req.bodyExpress-এর অভ্যাস; এখানে undefined আর TypeErrorচালিয়ে দেখুন
body += chunkবাংলা ভাঙেবাংলা body-তে পরীক্ষা
Body-র সীমা নেইMemory শেষ করার পথবড় body পাঠিয়ে 413 দেখুন
async handler-এ try/catch নেইএক request-এ process বন্ধভাঙা JSON পাঠান
Request-এর পথে readFileSync, bcrypt.hashSyncসবাই আটকে যায়“Sync” খুঁজুন, ভিড়ে ping মাপুন
Error-এর err.stack client-কে পাঠানোভেতরের তথ্য ফাঁসError response পড়ুন
“Node single-threaded, তাই thread নেই”, “timers সবার আগে”পুরোনো বা অর্ধেক সত্যnodejs.org-এর পাতা মিলিয়ে দেখুন
ছোট কাজে অকারণ packageবাড়তি নির্ভরতাnpm view, আসলেই আছে কি না দেখুন

AI-কে পর্যালোচক বানান। কোড চাইবার আগে শর্ত দিন, আর উত্তর পেলে নিজে চালিয়ে দেখুন। Secret বা আসল ডেটা কখনো পেস্ট করবেন না।

তুমি একজন Node.js backend reviewer. আমি Node [version লিখুন]-এ শুধু node:http দিয়ে লেখা একটি handler দিচ্ছি। কোনো npm package যোগ করতে বলবে না। [এখানে কোড পেস্ট করুন, কোনো secret ছাড়া] নিচের তালিকা ধরে দেখাও: ১. কোন লাইন event loop আটকাতে পারে, আর কেন? ২. Body পড়ায় সীমা আছে কি? UTF-8 লেখা ভাঙতে পারে কি? ৩. Async error কোথায় ধরা পড়ে না? ৪. Status code আর Content-Type ঠিক আছে কি? ৫. প্রতিটি সমস্যার জন্য একটি curl কমান্ড দাও, যা দিয়ে আমি নিজে সমস্যাটি দেখতে পারি। পুরো কোড আবার লিখবে না, শুধু যে লাইন বদলাতে হবে সেটি দেখাবে। Node-এর কোন docs পাতা থেকে বলছ, সেটিও লিখবে।

AI-এর লেখা server হাতে পেলে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১প্রতি সংযোগে একটি thread তো সহজ ছিল। Node-এর মতো event loop-এর দরকার পড়ল কেন?

১৯৯০-এর দশকের শেষে ওয়েব বড় হতে থাকলে একটি প্রশ্ন সামনে আসে: একটি server কি ১০ হাজার সংযোগ একসাথে ধরে রাখতে পারবে? Dan Kegel ১৯৯৯ সালের দিকে তাঁর লেখায় এর নাম দেন C10K সমস্যা। প্রতি সংযোগে একটি thread হলে প্রতিটি thread-এর stack-এর জন্য memory লাগে, আর এত thread-এর মধ্যে বদল করতে OS-এর সময় খরচ হয়। অথচ বেশিরভাগ সংযোগ সেই মুহূর্তে কিছু করছে না, শুধু অপেক্ষা করছে।

উত্তর এসেছিল OS থেকে। FreeBSD-তে kqueue আর Linux-এ epoll একটি thread-কে একসাথে হাজারো socket নজরে রাখার সুযোগ দিল। nginx-এর মতো server এই পথে চলে, আর ২০০৯ সালে Ryan Dahl সেই একই মডেল JavaScript-এ নিয়ে আসেন। তাঁর উপস্থাপনায় মূল কথা ছিল, disk আর network অপেক্ষা CPU-র তুলনায় বিশাল, তাই সেই অপেক্ষায় থ্রেড আটকে রাখা অপচয়।

তবে এটি বিনামূল্যে আসেনি। Thread-এর মডেলে একটি ধীর request শুধু নিজেকে থামায়, OS বাকিদের সময় দিয়ে দেয়। Event loop-এ scheduling সহযোগিতামূলক: প্রত্যেকে নিজে থেকে thread ছাড়লে তবেই চলে, তাই একজনের ভুলে সবাই আটকায়। এই পর্বের ৭৬ হাজার থেকে ৯৯ request/s-এর পতন সেই দামের ছবি। ২০১৮ সালে Dahl নিজেও JSConf EU-র একটি আলোচনায় Node-এর কিছু নকশা নিয়ে আফসোসের কথা বলেন, আর সেখানেই Deno-র কথা জানান। তবে সেই আফসোসের বিষয় event loop ছিল না, ছিল module ব্যবস্থা আর নিরাপত্তার মতো বিষয়।

মূল উপলব্ধি: Event loop thread-এর চেয়ে “ভালো” নয়। এটি memory আর অপেক্ষার খরচ কমায়, আর বিনিময়ে প্রতিটি লাইনের কোডের কাছে শৃঙ্খলা দাবি করে।

প্র ০২Java-র virtual thread আর Go-র goroutine এলে Node-এর event loop-এর সুবিধা কি শেষ?

পুরোপুরি নয়, তবে তুলনাটি বদলেছে। Go-তে goroutine আছে শুরু থেকে, আর Java 21 (২০২৩) virtual thread-কে পূর্ণ বৈশিষ্ট্য করেছে। দুই ক্ষেত্রেই আপনি সাধারণ, সোজা কোড লেখেন, যা দেখতে blocking; আর runtime গোপনে অপেক্ষার সময় আসল thread ছেড়ে দেয়। অর্থাৎ ভেতরে একই kqueue বা epoll-এর কৌশল, বাইরে thread-এর সরল চেহারা।

এতে Node-এর কিছু সুবিধা কমে: Node-এ await লিখতে হয়, আর তার “রং” পুরো call chain-এ ছড়ায়। Go আর Java-য় অনেক core একসাথে ব্যবহার সহজ। CPU-র ভারী কাজে একটি handler অন্যদের সহজে থামায় না, কারণ runtime সব core জুড়ে কাজ ভাগ করে।

অন্যদিকে Node-এর নিজের শক্তি অন্য জায়গায়। Frontend আর backend-এ একই ভাষা, একই type, এমনকি একই validation schema ভাগ করা যায়, যা পর্ব ২১-এ দেখেছেন। npm-এর বিশাল ecosystem আছে। আর এক thread-এ shared memory নিয়ে গোলমাল অনেক কম: দুটি await-এর মাঝে আপনার কোড কেউ থামায় না, যেখানে Java বা Go-তে data race নিয়ে সবসময় সাবধান থাকতে হয়।

দুই পথেই অপেক্ষা-প্রধান API ভালো চলে। সিদ্ধান্ত সাধারণত হয় দলের দক্ষতা, code ভাগ করার প্রয়োজন আর কাজের ধরন দেখে, শুধু benchmark দেখে নয়। Stack বাছাই নিয়ে পুরো আলোচনা আসবে পর্ব ৮৫-তে। আপনি যদি ভিন্ন মডেল দেখতে চান, Java-র virtual thread পর্বটি পড়তে পারেন।

মূল উপলব্ধি: নতুন ভাষাগুলো event loop-কে বাদ দেয়নি; তারা সেটিকে runtime-এর ভেতরে লুকিয়েছে। প্রশ্নটি “কে অপেক্ষা সামলায়” থেকে সরে গেছে “কোড লেখা আর দল চালানো কোথায় সহজ”-এ।

প্র ০৩একটি regular expression কি পুরো server ফেলে দিতে পারে?

হ্যাঁ, আর এটি বাস্তবে ঘটেছে। বেশিরভাগ regex engine, JavaScript-এরটিও, backtracking করে: মিল না পেলে পিছিয়ে এসে অন্য পথ চেষ্টা করে। (a+)+$-এর মতো nested quantifier-এ কিছু ইনপুটে পথের সংখ্যা ইনপুটের দৈর্ঘ্যের সাথে সূচকীয়ভাবে বাড়ে। অর্থাৎ কয়েক ডজন অক্ষরের একটি লেখাই CPU-কে অনেকক্ষণ আটকে রাখতে পারে। এর নাম ReDoS (Regular expression Denial of Service).

সব সিস্টেমেই এটি ক্ষতিকর, কিন্তু Node-এ বিশেষভাবে। Thread-প্রতি-request server-এ একটি thread আটকায়; Node-এ পুরো loop আটকায়, তাই একটি মাত্র request পুরো সেবা থামাতে পারে। Regex সাধারণ কোডের মতো দেখায়, Sync শব্দও থাকে না, তাই code review-তে চোখ এড়ায়।

দুটি প্রকাশ্য ঘটনা উদাহরণ হিসেবে প্রায়ই বলা হয়। Stack Overflow ২০১৬ সালে জানায়, একটি লেখার ফাঁকা অংশ ছাঁটার regex-এ অস্বাভাবিক ইনপুট আসায় তাদের সেবা কিছু সময় বন্ধ ছিল। Cloudflare ২০১৯ সালের জুলাইয়ে তাদের postmortem-এ জানায়, একটি WAF নিয়মের regex CPU-কে পূর্ণ মাত্রায় ব্যস্ত করে ফেলেছিল। এ দুটি Node-এর ঘটনা নয়, তবে যন্ত্রটি একই।

প্রতিরোধের উপায়ও জানা। Node-এর “Don't block the event loop” পাতা বলে, nested quantifier আর overlapping OR এড়ান; সরল মিলের জন্য indexOf নিন। ইনপুটের দৈর্ঘ্য আগে সীমিত করুন, কারণ সূচকীয় বৃদ্ধি ছোট ইনপুটে বিপজ্জনক হয় না। RE2-এর মতো ইঞ্জিন backtracking করে না, যদিও কিছু বৈশিষ্ট্য ছাড়তে হয়। স্বয়ংক্রিয় checker সাহায্য করে, কিন্তু Node-এর পাতাই সতর্ক করে, এরা সব ধরতে পারে না। AI-এর লেখা regex-ও একই পরীক্ষায় ফেলুন।

মূল উপলব্ধি: ব্যবহারকারীর ইনপুট যদি কাজের পরিমাণ ঠিক করে, তবে সেই কাজের একটি সীমা থাকতেই হবে। এটি regex, JSON আর বড় body সবার ক্ষেত্রেই সত্য।

প্র ০৪Thread pool ৪ জনের কেন? সবসময় ৬৪ করে দিলে সমস্যা কী?

৪ একটি সাধারণ ডিফল্ট, কোনো প্রাকৃতিক নিয়ম নয়। Pool বেশিরভাগ server-এ মূল পথে থাকে না, কারণ network socket এখানে আসে না। তাই ছোট ডিফল্টে memory আর চালুর খরচ কম থাকে। এই পর্বের মাপে ৪ থেকে ৮ করলে ৮টি hash ৪০৭ ms থেকে ২৭৪ ms-এ নেমেছে, কাজেই বাড়ানো কখনো সত্যিই কাজে লাগে।

কিন্তু কাজের ধরন দেখে সিদ্ধান্ত নিতে হয়। Pool-এ দুই রকম কাজ থাকে। CPU-ভারী কাজে (pbkdf2, zlib) core-এর সংখ্যার চেয়ে বেশি thread দিলে লাভ কমে, কারণ thread-গুলো একই core-এর জন্য লড়াই করে। ৮ core-এর machine-এ ৬৪ thread মানে শুধু বেশি context switch. অপেক্ষা-প্রধান কাজে (ধীর network disk-এ ফাইল, dns.lookup) বেশি thread লাভজনক হতে পারে।

একটি গোপন ফাঁদ হলো dns.lookup. Node-এর dns ডকুমেন্টেশন সতর্ক করে, এটি JavaScript-এ async দেখালেও ভেতরে OS-এর getaddrinfo-কে sync ভাবে ডাকে, thread pool-এ। fetch বা HTTP client হোস্টের নাম খুঁজতে সাধারণত এটিই ব্যবহার করে। DNS ধীর হলে ৪টি thread সেই অপেক্ষায় বসে থাকে, আর আপনার fs.readFile লাইনে দাঁড়ায়। লক্ষণটি অদ্ভুত: DNS-এর সমস্যায় ফাইল পড়া ধীর। সমাধান হতে পারে pool বাড়ানো, DNS cache, বা উপযুক্ত ক্ষেত্রে dns.resolve, যা pool ব্যবহার করে না।

সংখ্যা বদলানোর আগে মাপুন। পরিবেশে UV_THREADPOOL_SIZE বসিয়ে একই চাপে p99 দেখুন, শুধু গড় নয়। Node-এর ডকুমেন্টেশন অনুযায়ী এটি process চালুর আগেই ঠিক করতে হবে। আর মনে রাখুন, pool ছোট হওয়ার চেয়ে বড় সমস্যা হতে পারে ভারী কাজটি ভুল জায়গায় থাকা। ঘন ঘন hash বা ছবি বানানোর কাজ আলাদা service বা queue-তে গেলে pool নিয়ে ভাবতেই হয় না।

মূল উপলব্ধি: Thread সংখ্যা কোনো জাদুর বোতাম নয়। আগে জানুন লাইনে কে দাঁড়িয়ে আছে: CPU-র কাজ, না অপেক্ষার কাজ।

প্র ০৫Express, Fastify, Hono থাকতে raw node:http শেখা বা ব্যবহার করার কোনো অর্থ আছে?

Production API-তে raw node:http সাধারণত প্রথম পছন্দ নয়, আর হওয়া উচিতও নয়। এই পর্বেই দেখলেন, দুটি resource-এর জন্যই সীমা, 405, error আর log হাতে লিখতে হলো। Framework এই কোড পরীক্ষিত আকারে দেয়, আর পাশে আসে middleware-এর ecosystem. Express ২০১০ সালের দিক থেকে প্রচলিত, আর Express 5 চূড়ান্ত হয় ২০২৪ সালে। Fastify গতি আর schema-র দিকে জোর দেয়। Hono Web standard-এর Request আর Response-এর ওপর গড়া, তাই একাধিক runtime-এ চলতে পারে। কোনটি সেরা, তা নিয়ে সমাজে একমত হওয়া নেই; প্রয়োজন দেখে ঠিক করতে হয়।

তবু raw শেখার তিনটি বাস্তব কারণ আছে। প্রথমত, সব framework-এর নিচে এই একই req, res, stream আর event loop. Express-এ req.body কেন undefined, keep-alive-এ কেন process বন্ধ হচ্ছে না, বা server কেন ঝুলে আছে, এসব ধরতে নিচের স্তর জানা লাগে। দ্বিতীয়ত, কিছু ছোট কাজে dependency ছাড়া চলাই ভালো: health check, একটি webhook গ্রাহক, বা test-এর ভেতরে নকল server. প্রতিটি package আপডেট আর নিরাপত্তার দায় আনে। তৃতীয়ত, performance-এর কঠিন সমস্যায় framework-এর স্তর সরিয়ে আসল খরচ মাপতে হয়।

একটি বদলও লক্ষ করার মতো। আধুনিক ধারায় Next.js-এর route handler (পর্ব ৫৭) আর অনেক runtime Request ও Response-এর মতো Web standard ব্যবহার করে। অর্থাৎ res.end হয়তো চোখে পড়বে কম, কিন্তু stream, status আর event loop-এর ধারণা থেকেই যাবে।

মূল উপলব্ধি: নিচের স্তর শিখুন framework বাদ দেওয়ার জন্য নয়, বরং framework-এর ভেতরে কী হচ্ছে তা বোঝার জন্য।

অনুশীলন

হিসাব, কোড পড়া আর নিজে চালানো: এক একটি করে করুন।

  1. হিসাব: “বাজারঘর”-এর order API-তে প্রতি request-এ CPU 8 ms আর database অপেক্ষা 72 ms. (ক) এক process-এর সর্বোচ্চ হার কত? (খ) সেই হারে একসাথে কতগুলো request চলমান থাকবে? (গ) সন্ধ্যায় ৪৫০ request/s আসে; অন্তত কতটি process লাগবে?

    (ক) শুধু CPU সময় গোনা হয়: 1 ÷ 0.008 = ১২৫ request/s. অপেক্ষা হার কমায় না, কারণ তখন thread মুক্ত।

    (খ) মোট সময় W = 8 + 72 = 80 ms = 0.08 s. Little’s Law: L = 125 × 0.08 = ১০টি।

    (গ) 450 ÷ 125 = 3.6, তাই অন্তত ৪টি process. ভিড়ের জন্য ফাঁকা রাখতে প্রতিটি process ৭০%-এ চালাতে চাইলে 450 ÷ (125 × 0.7) ≈ 5.1, অর্থাৎ ৬টি। আর database-কে 450 × 72 ms ≈ ৩২টি চলমান query নিতে হবে, যা আলাদা করে দেখতে হবে।

  2. Critique: নিচের handler-এ অন্তত পাঁচটি সমস্যা খুঁজুন।
    http.createServer(async (req, res) => {
      let body = "";
      req.on("data", (c) => (body += c));
      await new Promise((ok) => req.on("end", ok));
      const order = JSON.parse(body);
      const list = JSON.parse(fs.readFileSync("orders.json", "utf8"));
      list.push(order);
      fs.writeFileSync("orders.json", JSON.stringify(list));
      res.end("saved");
    }).listen(3000);
    • সীমা নেই: Body যত বড়ই হোক জমতে থাকে।
    • বাংলা ভাঙে: body += c টুকরো ভেঙে লেখা বানায়।
    • Crash: ভাঙা JSON-এ async handler reject হয়, আর ডিফল্টে পুরো process বন্ধ হয়।
    • Blocking: readFileSync, writeFileSync, বড় JSON.parse আর stringify loop আটকায়, আর ফাইল বড় হলে ক্রমে আরও ধীর হয়।
    • যাচাই নেই: Method, path আর মানের কোনো পরীক্ষা নেই, GET-ও order লিখবে।
    • উত্তর ভুল: Status 200, plain text; হওয়া উচিত 201 আর JSON.
    • ফাইল database নয়: Crash-এর মাঝপথে ফাইল নষ্ট হতে পারে। এই জন্যই মডিউল ৬-এ database আসছে।
  3. আউটপুট অনুমান করুন: CommonJS ফাইলে নিচের প্রোগ্রাম কোন ক্রমে ছাপবে?
    const fs = require("node:fs");
    console.log("A");
    fs.readFile(__filename, () => {
      setTimeout(() => console.log("B"), 0);
      setImmediate(() => console.log("C"));
      process.nextTick(() => console.log("D"));
      Promise.resolve().then(() => console.log("E"));
      console.log("F");
    });
    console.log("G");

    সত্যিকারের run-এ: A G F D E C B.

    মূল script-এর sync লাইন আগে: A, G. ফাইল পড়া শেষ হলে poll phase-এ callback: F. Callback শেষে nextTick (D), তারপর promise (E). I/O callback-এর ভেতর থেকে ডাকা হয়েছে, তাই setImmediate (C) নিশ্চিতভাবে setTimeout (B)-এর আগে।

  4. কমান্ড: “বাজারঘর” API-তে DELETE /api/products/:id যোগ করুন। সফল হলে 204, না পেলে 404. তারপর curl দিয়ে একই পণ্য দুবার মুছুন, আর PUT পাঠিয়ে Allow header দেখুন।

    products.js-এ removeProduct(id) লিখুন, যা findIndex আর splice দিয়ে মোছে আর true বা false ফেরত দেয়। routes.js-এ:

    function remove(req, res, ctx) {
      if (!removeProduct(Number(ctx.params[0]))) throw new HttpError(404, "product not found");
      res.writeHead(204).end();
    }
    // route table-এ
    { pattern: /^\/api\/products\/(\d+)$/, methods: { GET: getOne, DELETE: remove } },

    সত্যিকারের run-এ ফল:

    $ curl -s -i -X DELETE $B/2 | head -1
    HTTP/1.1 204 No Content
    $ curl -s -i -X DELETE $B/2 | head -1
    HTTP/1.1 404 Not Found
    $ curl -s -i -X PUT $B/1 | grep -E "HTTP|Allow"
    HTTP/1.1 405 Method Not Allowed
    Allow: GET, DELETE

    লক্ষ করুন, Allow নিজে থেকেই বদলেছে, কারণ সেটি route table থেকে তৈরি হয়। একটি জায়গায় তথ্য রাখার সুবিধা এখানেই।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি Node.js server-এ POST request-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Client-এর bytes OS kernel-এর socket-এ পৌঁছায়
  2. Event loop-এর poll phase-এ socket-এর খবর আসে
  3. HTTP parser header পড়ে req বানায়
  4. Handler চলে, আর stream থেকে body-র টুকরো জমায়
  5. res.end উত্তর পাঠিয়ে কাজ শেষ করে

Framework ছাড়া node:http server-এ JSON POST-এর body পেতে কী করতে হয়?

  • সরাসরি req.body পড়তে হয়
  • Stream থেকে Buffer জমিয়ে জোড়া দিয়ে JSON.parse করতে হয়
  • req.headers.body পড়তে হয়
  • আবার fetch ডাকতে হয়

একটি Node.js handler-এর ভেতর থেকে setTimeout(a, 0), setImmediate(b), Promise.resolve().then(c) আর process.nextTick(d) এই ক্রমে ডাকা হলো। কোন ক্রমে চলবে?

  • a b c d
  • c d b a
  • d c b a
  • d c a b

নিচের কোন কাজটি ডিফল্টে libuv-এর thread pool ব্যবহার করে না?

  • fs.readFile
  • crypto.pbkdf2
  • dns.lookup
  • একটি TCP socket থেকে HTTP request পড়া

একটি Node process-এর handler প্রতি request-এ ২০ ms sync CPU নেয়, আর কোনো অপেক্ষা নেই। ১০০টি সংযোগ একসাথে চাপ দিলে আনুমানিক কত হবে?

  • ২০০০ request/s, latency ২০ ms
  • ৫০ request/s, latency প্রায় ২ সেকেন্ড
  • ৫০ request/s, latency ২০ ms
  • ৫০০ request/s, latency ২০০ ms

২০টি request, প্রতিটি database-এর জন্য ১০০ ms অপেক্ষা করে, আর সবগুলো মিলে ১৩০ ms-এ শেষ হলো। Node-এর একটি thread-ই ছিল। এটি কীসের উদাহরণ?

  • Concurrency: অপেক্ষার কাজ একে অপরের সাথে মিশে গেছে
  • Parallelism: ২০টি core একসাথে কাজ করেছে
  • Thread pool ২০টি thread বানিয়েছে
  • Blocking call

একটি Node.js API-তে ভারী ছবি প্রক্রিয়া চলার সময় ছোট /health route-ও ধীর হয়ে যাচ্ছে। সবচেয়ে উপযুক্ত প্রথম সমাধান কোনটি?

  • কাজটির সামনে await লিখে দেওয়া
  • UV_THREADPOOL_SIZE বাড়ানো
  • কাজটি worker_threads বা আলাদা background job-এ সরানো
  • Keep-alive বন্ধ করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:

মনে রাখুন

  1. http.createServer((req, res) => …) দিয়ে server; res.end না ডাকলে উত্তর শেষ হয় না।
  2. Body একটি stream: Buffer জমান, সীমা রাখুন, শেষে Buffer.concat.
  3. Handler-এর পরে ক্রম: nextTick → promise → setImmediate → timer.
  4. Socket যায় OS-এ; ফাইল, dns.lookup, hash যায় ৪ জনের thread pool-এ।
  5. এক process-এর সর্বোচ্চ হার ≈ 1 ÷ CPU সময়। Sync কাজ সবাইকে আটকায়, async কাজ অন্যদের আটকায় না।

পরের পর্বে: এই পর্বের হাতে লেখা route table, regex আর sendJson-কে আমরা Express-এ নিয়ে যাব। দেখবেন কতটা কোড মুছে যায়, আর কোনটি রয়ে যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান