48 questions
Node.js interview questions and answers
Junior questions
1. Why do we need a backend if we already have a frontend?
Answer
The browser cannot be trusted: anyone can change the code running in it, so checks of permissions, prices and data must live on the server. The backend holds the business logic, database access and secrets that must never reach the client. It also gives one source of truth for several clients: the website, the mobile app, integrations. In a shop, for example, the frontend shows the cart, while the backend calculates the total and decides whether the order can be placed.
Follow-up question: What is business logic, using an online shop as an example?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: developer.mozilla.org
2. Node.js runs JavaScript in a single thread. How does it serve many requests at once, and what can get in its way?
Answer
Most of the time a request is waiting for a database or the network, and Node takes the next request in the meantime. When the result is ready, the event loop returns to the first request and runs its callback. That is why one thread copes with thousands of connections as long as the work is mostly I/O. Heavy computation in JavaScript gets in the way: while it runs, all the other requests wait.
Follow-up question: What does it mean to block the event loop?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
3. How do CommonJS and ES modules differ in Node.js, and how does Node decide which format a file uses?
Answer
CommonJS uses
requireandmodule.exports, ES modules useimportandexport. Node treats a file as an ES module if it has the.mjsextension or the nearestpackage.jsonhas"type": "module". Otherwise it is CommonJS. ES modules load asynchronously and support top-levelawait, while CommonJS has__dirnameand__filename, which ESM lacks. New projects are usually written in ESM.Follow-up question: How do you get the path of the current directory in an ES module file?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
4. How should a Node.js app handle configuration and secrets, such as a database connection string?
Answer
Configuration comes from environment variables via
process.envinstead of being hardcoded. A.envfile is handy locally, but it goes into.gitignoreand is never committed. In production the values are set by the platform: a container, CI or a secrets manager. The variables are validated at startup so the app fails right away with a clear error instead of on the first request. All values inprocess.envare strings, so numbers and booleans must be parsed explicitly.Follow-up question: What do you do if a secret accidentally ended up in git history?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
5. In NestJS with no custom filters, a controller throws a
NotFoundException, a plainErrorwith text about a database password, and anHttpExceptionwith an object and status 429. What does the client get in each case?Answer
NotFoundExceptiongives 404 with a body containingstatusCode,messageanderror. Nest treats a plainErroras unexpected: the client gets 500 and a genericInternal server error, and the error text goes only to the log, not the body.HttpExceptionwith an object returns 429, and the passed object becomes the response body as is. This is done by the built-in exception filter, and for a uniform error format it is replaced with a custom one.Follow-up question: How do you write a custom global filter with DI, and why do you need
APP_FILTER?- Knowledge base: NestJS
- Source: docs.nestjs.com
6. How do you add Swagger (OpenAPI) to NestJS, and how do you avoid writing
@ApiPropertyon every DTO field?Answer
The
@nestjs/swaggerpackage is set up inmain.ts:DocumentBuilderassembles the description,SwaggerModule.createDocumentbuilds the document, andSwaggerModule.setupserves the UI. DTO fields are described with@ApiProperty. To avoid writing it by hand, you enable the CLI plugin innest-cli.json: it adds annotations at build time from TypeScript types and comments. For derived DTOs, helpers likePartialTypemust be imported from@nestjs/swagger, otherwise the schema loses its metadata.Follow-up question: How do you serve the OpenAPI JSON to the frontend to generate types, and what do you do with Swagger in production?
- Knowledge base: NestJS
- Source: docs.nestjs.com
Middle questions
1. Name the phases of the Node.js event loop in order and say what runs in each. Where do
process.nextTickand promises fit in this picture?Answer
The phases go like this: timers (
setTimeout,setInterval), pending callbacks, idle/prepare (internal), poll (new I/O events, where the loop may wait), check (setImmediate) and close callbacks.process.nextTickand promises are not phases. After every callback Node first drains thenextTickqueue, then the microtask queue, and only then moves on. That is why an endless chain ofnextTickcalls stops the whole loop.Follow-up question: Why do these queues run after every single timer rather than after the whole timers phase?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
2. A CommonJS file calls
setTimeout(..., 0),setImmediate,process.nextTick,Promise.resolve().thenand a synchronousconsole.log. What is the output order, and which part of it is guaranteed?Answer
The first three are guaranteed: synchronous code first, then the
nextTickqueue, then microtasks, that is the promise. The order ofsetTimeoutandsetImmediatefrom the main module is not guaranteed. The Node docs say it depends on process performance: if 1 ms has already passed by the first timers phase, the timer fires first, otherwisesetImmediatedoes. Inside an I/O callback the order is deterministic:setImmediatealways runs before the timer.Follow-up question: How can you make the order of the timer and
setImmediatedeterministic without changing the calls themselves?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
3. What does the
falsereturned bywrite()on a Writable stream mean, and what happens if you ignore it?Answer
write()returnsfalsewhen the internal buffer has reachedhighWaterMark. No data is lost: it is a backpressure signal telling you to stop writing and wait for thedrainevent. If you ignore it, a fast source keeps writing into memory, the buffer grows, and the process can crash with OOM.pipelinedoes this pausing automatically. The defaulthighWaterMarkis 64 KB for byte streams and 16 objects in object mode.Follow-up question: How does this work when the source is a
for awaitover a database cursor instead ofpipe?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
4. Compare
cluster,child_processandworker_threads. What would you choose for a CPU-heavy task, and what for using all cores with an HTTP server?Answer
clusterstarts separate processes that share one port, which is how an HTTP server uses all cores, and memory is not shared between processes.worker_threadsare threads in one process, each with its own V8 isolate and event loop, and they suit CPU-heavy work such as hashing, parsing and compression. For I/O, workers give almost no benefit.child_processis for running a separate program or isolating crashes. In Kubernetesclusteris usually unnecessary: it is simpler to run one process per container and add more pods.Follow-up question: What happens to an in-memory cache or rate limiter when the service runs in 4 processes?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
5. Why does
--max-old-space-sizenot protect a Node.js process from a memory blowup when a largeBufferis allocated? Which memory metrics should you watch?Answer
The contents of a
Bufferlive outside the V8 heap, and only a small wrapper object lands in the heap. SoheapUsedbarely changes whilearrayBuffersandrssgrow. The--max-old-space-sizeflag limits only the old space in V8 and does not count buffers, compression or native modules. A process can have a 200 MB heap and 2 GB of RSS, and the container gets killed by the OOM killer without any JavaScript heap out of memory error. That is why you watchrss,heapUsed,externalandarrayBufferstogether.Follow-up question: RSS keeps growing while
heapUsedis stable. Where do you look next?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
6. Inside
try/catchyou call[1, 2, 3].forEach(async (id) => { await save(id); })andsave(2)throws. Why doescatchnot fire, and how do you process the array correctly, sequentially and in parallel?Answer
forEachcalls the callback and discards the returned promise, so the function waits for nothing:doneis printed before the error, and the rejection becomes anunhandledRejection. This is a typical cause of a worker that finished "successfully" while some records were not saved. Sequentially you process withfor...ofandawait, in parallel withawait Promise.all(ids.map(save)). For thousands of items you need a concurrency limit, otherwise the connection pool is exhausted. If you need all results together with errors, usePromise.allSettled.Follow-up question:
Promise.allrejected on the first error. What happens to the remaining requests?- Course: Node.js: getting started
- Knowledge base: Node.js, JavaScript
- Source: nodejs.org
7. A service calls an external API thousands of times per minute. What do you configure on the HTTP client, and why is a missing timeout dangerous?
Answer
Keep-alive removes the TCP and TLS handshake on every request, so connections should be reused. The number of sockets should be limited, because
maxSocketsis unlimited by default. Without a timeout, a hung dependency accumulates open requests, memory and connections, and someone else's problem becomes yours. Because of the keep-alive race,ECONNRESETis possible, so you may automatically retry only idempotent requests. The built-infetchruns on its own pool (undici), andhttp.Agentsettings do not affect it.Follow-up question: Where do you put a circuit breaker and how does it relate to timeouts?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
8. In CommonJS, module
adoesrequire('./b')andbdoesrequire('./a'). Why is there no infinite loop or error, and what trap does this create?Answer
CommonJS puts a module into the cache before its execution finishes. When
basks fora, it receives the existing but unfinishedexportsobject ofa, not a second run. That is why there is no loop, andbsees only whatamanaged to export before callingrequire. The trap is that top-level destructuring locks in stale or missing values. It is fixed by moving shared code into a third module or by accessing the export lazily inside a function.Follow-up question: How does the same circular dependency work in ESM, and why can you get a
ReferenceErrorthere?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
9. A module exports a counter
nand a functionincthat doesn++. What does the importer see after callinginc()in CommonJS, and what in ES modules? Why the difference?Answer
In CommonJS
module.exportsis a plain object, and the value ofnis copied into it when it is created. Afterinc()the destructured variable stays0. In ESM imports are live bindings: the imported variable points to the variable in the exporting module, so afterinc()we see1. An importer cannot reassign the imported variable, only the exporter itself can change it.Follow-up question: Why is there no
__dirnamein ESM and what replaces it?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
10. Where in NestJS do you put JWT and role checks, DTO validation, response time measurement, a uniform error format and response caching? Why might a global guard via
useGlobalGuards(new AuthGuard())not work?Answer
Authorization is a guard, because it sees
ExecutionContextand the route metadata, which middleware does not know. Validation is done by a pipe, for exampleValidationPipewithwhitelist. Response time and caching belong in an interceptor, since it wraps the handler call. A uniform error format is an exception filter. A guard created viauseGlobalGuards(new ...)inmain.tsdoes not go through DI, so its dependencies are not injected. The correct way is to register it as a provider with theAPP_GUARDtoken in a module.Follow-up question: How do you make a route public when there is a global guard?
- Knowledge base: NestJS
- Source: docs.nestjs.com
11. In NestJS,
UsersServicedepends onMailService, which is declared inprovidersofMailModule, andUsersModulehasimports: [MailModule]. The app does not start withNest can't resolve dependencies. Why, and how do you fix it?Answer
A module in Nest is a visibility boundary: a module's providers are private until they are added to
exports. The entryimports: [MailModule]gives access only to what that module exports, andMailModuleexports nothing. That is whyUnknownDependenciesExceptionappears. The one-line fix is to addexports: [MailService]toMailModule. DuplicatingMailServicein theprovidersofUsersModuleis not advisable, because that creates a second instance.Follow-up question: What is a dynamic module, and when would you make a module
@Global()?- Knowledge base: NestJS
- Source: docs.nestjs.com
12. What kinds of custom providers exist in NestJS and what is each for? What does the entry
providers: [UsersService]really mean?Answer
The entry
providers: [UsersService]is shorthand for{ provide: UsersService, useClass: UsersService }.useValuegives a ready value or a mock, for example in tests.useClasslets you swap the implementation depending on the environment.useFactorycreates a provider with logic and dependencies viainjectand can be asynchronous, for example to connect to a database.useExistingcreates an alias for an existing provider without a second instance.Follow-up question: How do you inject a value with a string token, and what changes if
useFactoryreturns a Promise?- Knowledge base: NestJS
- Source: docs.nestjs.com
13. In NestJS,
CheckoutServicedepends on the interfacePaymentGateway, andproviderscontains the classLiqPayGatewaythat implements it. Startup fails withNest can't resolve dependencies. Why, and how do you fix it while keeping the dependency on an abstraction?Answer
TypeScript interfaces do not exist at runtime, so Nest has no token: in the constructor metadata an interface is recorded simply as
Object. You need an explicit token: aSymbol, a string or an abstract class. You register{ provide: PAYMENT_GATEWAY, useClass: LiqPayGateway }and write@Inject(PAYMENT_GATEWAY)in the constructor. That way the service depends on an abstraction, and the implementation is easy to swap in tests or by config.Follow-up question: What breaks if you import the service with
import type?- Knowledge base: NestJS, TypeScript
- Source: docs.nestjs.com
14. In what order are lifecycle hooks called in NestJS at startup and at shutdown? Where is it better to open a database connection: in the constructor or in
onModuleInit?Answer
The constructor runs when the app is created, while
onModuleInitandonApplicationBootstrapare called atlistenorinit. Startup order: constructor,onModuleInit,onApplicationBootstrap. Shutdown goes:onModuleDestroy,beforeApplicationShutdown,onApplicationShutdown. An asynconModuleInitdelays startup, because Nest waits for its Promise. That is why connections are opened inonModuleInitand not in the constructor, which cannot be async. System signals work only afterenableShutdownHooks().Follow-up question: Are these hooks called for a request-scoped class?
- Knowledge base: NestJS
- Source: docs.nestjs.com
15. Name the full request processing chain in NestJS in the correct order. Where do exception filters work?
Answer
The order is: middleware, guards, interceptors (the part before the handler), pipes, the handler itself, then interceptors on the way back. A pipe runs after an interceptor, not before it. An interceptor wraps the handler call: code before
next.handle()runs on the way in, code intapormapon the way out. Exception filters fire only when an exception was thrown and not caught somewhere in this chain. Within each type the order is: global, then controller, then route.Follow-up question: Why are exception filters looked up in reverse, from route to global?
- Knowledge base: NestJS
- Source: docs.nestjs.com
16. What do the
whitelist,forbidNonWhitelistedandtransformoptions do in the NestJSValidationPipe? What happens to an extrarolefield in the body and topage=2in the query?Answer
With
whitelist: truethe pipe silently strips fields that have no validation decorators, so the extrarolein the body disappears.forbidNonWhitelisted: truereturns 400 saying the property should not exist instead of stripping it. Withtransform: truethe pipe turns a plain object into an instance of the DTO class, and with@Type(() => Number)the stringpage=2from the query becomes a number. Withoutwhitelistextra fields pass through and create a mass assignment vulnerability.Follow-up question: Why is the value in the DTO not a class instance without
transform?- Knowledge base: NestJS, TypeScript
- Source: docs.nestjs.com
17.
ValidationPipeis enabled, butPOST /orderswithitems: [{ sku: 5, qty: -1 }]returns 201.CreateOrderDtohas only@IsArray(). Why did theItemDtorules not fire, and what should you add?Answer
@IsArray()only checks that the value is an array and does not look inside the elements. TheItemDto[]type in TypeScript disappears at runtime, so the validator does not know what these elements are. You need@ValidateNested({ each: true })to validate every element, and@Type(() => ItemDto)fromclass-transformerso the elements become class instances with metadata. Without@Typethe elements stay plain objects and the decorators on them have no effect.Follow-up question: How do you limit the array size and validate a union in a nested field?
- Knowledge base: NestJS, TypeScript
- Source: docs.nestjs.com
18. How do you organize configuration in NestJS so the app does not start without
DATABASE_URLinstead of failing on the first request?Answer
You add
ConfigModule.forRoot({ isGlobal: true })and pass avalidationSchema(for example Joi) or avalidatefunction: environment variables are checked at startup and the app fails right away with a clear error. In code you useConfigService.getOrThrow, which throws if the value is missing. WithregisterAsthe config is grouped into a namespace. Note thatregisterAsvalidates nothing by itself, the check is still done by the schema orvalidate.Follow-up question: How do you pass the config to
TypeOrmModule.forRootAsyncand change a value in a test?- Knowledge base: NestJS
- Source: docs.nestjs.com
19. Walk a request through authentication with Passport and JWT in NestJS, from login to
req.userin the controller. What must never go into the token?Answer
At login the server compares the password with the hash and signs a JWT via
JwtService. On a protected routeAuthGuard('jwt')runsJwtStrategy: it takes the Bearer token from the header and verifies the signature andexp. Thenvalidate(payload)is called and its result becomesreq.user. A JWT is signed but not encrypted, so its contents can be read without a key, and passwords and sensitive data are never put into it. Authentication answers "who are you", authorization answers "what are you allowed to do".Follow-up question: How do you revoke an already issued access token?
- Course: Auth and security
- Knowledge base: NestJS, JWT
- Source: docs.nestjs.com
20. All endpoints of a NestJS API must require a JWT by default, except
/healthand/auth/login. How do you do this so a new controller cannot be accidentally left open?Answer
You register a global guard through the
APP_GUARDprovider instead of@UseGuardson every controller, so protection applies by default. Exceptions are marked with a custom@Public()decorator that sets a flag viaSetMetadata. The guard reads it withReflector.getAllAndOverrideon the handler and the class. Payment provider webhooks are also public as far as JWT is concerned, but they must verify the request signature. On top of that you write a test that walks all routes and expects 401 without a token, except for an allowlist.Follow-up question: How do you support both JWT for users and API keys for partners in the same guard?
- Course: Auth and security
- Knowledge base: NestJS, JWT
- Source: docs.nestjs.com
21. What is a dynamic module in NestJS? How do
forRoot,forFeatureandregisterdiffer, and when is@Global()appropriate?Answer
A dynamic module is a module that returns a
DynamicModulewith providers and exports configured at import time.forRootis called once at the root for shared configuration,forFeatureis used in a feature that relies on an entity already configured by the root, andregistergives a separately configured instance for each import.forRootAsyncwithuseFactoryandinjectis needed when the config depends on other providers, for exampleConfigService.@Global()is justified for truly cross-cutting things: config, logger, database connection. In other cases it hides dependencies.Follow-up question: How do you write your own module for a payment provider client with
forRootAsync?- Knowledge base: NestJS
- Source: docs.nestjs.com
22.
OrdersModuleimportsUsersModule, andUsersModulealready importedOrdersModule, and NestJS crashes at startup with an error aboutundefinedinimports. How do you fix it quickly, and how properly?Answer
A cycle between modules or between files (often through a barrel
index.ts) makes the classundefinedat the moment the decorator is applied. The quick fix isforwardRef(() => Module)in both modules and@Inject(forwardRef(...))for providers. But the creation order in a cycle is undefined, so you cannot rely on a dependency in the constructor. It is better to break the cycle: move shared logic into a third module or reverse the dependency via events. Cycles between files are found with a tool likemadge --circular.Follow-up question: Does the caller of
emitlearn about an error in an async EventEmitter2 listener?- Knowledge base: NestJS
- Source: docs.nestjs.com
23. A money transfer in NestJS "half succeeds": the debit happened, the credit failed, but there is no rollback. Inside
dataSource.transaction(...)some queries go through an injected repository. What is the cause?Answer
The injected repository works through a different connection from the pool, so its queries run outside the transaction and rollback does not touch them. All operations inside the transaction must go through the
managergiven by the callback:manager.decrement(...),manager.save(...). Rollback undoes only what went through thatmanager. In Prisma the rule is the same: queries go throughtxin$transaction(async (tx) => ...), not through the global client.Follow-up question: How do you pass the transaction through several services without dragging
managerinto every method?- Course: Prisma ORM
- Knowledge base: NestJS, PostgreSQL, Prisma
- Source: typeorm.io
24. Services throw domain errors
OrderNotFoundandInsufficientFunds, and Prisma throwsP2002on a duplicate. How do you design error handling in NestJS so the client gets correct statuses in a uniform format?Answer
Domain errors do not extend
HttpException, so the domain layer does not depend on HTTP. One global exception filter maps error types to statuses:OrderNotFoundto 404,InsufficientFundsto 422 or 409, PrismaP2002to 409,P2025to 404. The response body has a uniform format, for example Problem Details with arequestId. Unknown errors are returned as 500 with a generic text, and the full stack trace goes only to the log. The filter is registered viaAPP_FILTERso DI works.Follow-up question: How do you handle the same set of domain errors in a queue consumer, where there are no HTTP statuses?
- Knowledge base: NestJS, Prisma
- Source: docs.nestjs.com
25. A colleague says: "JavaScript is an interpreted language, so Node is always slower than Go." What actually happens to a function in V8, from source text to execution on the CPU?
Answer
The parser builds an AST, the Ignition interpreter compiles it into bytecode and runs it. V8 moves hot functions to higher JIT tiers: Sparkplug (a fast baseline), Maglev (a mid-tier optimizer) and TurboFan (the strongest optimizations). The move is decided by call counters and type feedback, and Maglev and TurboFan compile on background threads. Optimized code is speculative: when an assumption about types breaks, deoptimization goes back to Ignition. That is why hot code with stable types runs close to native speed.
Follow-up question: What exactly does optimized code check before the fast path, and what happens when the check fails?
- Knowledge base: Node.js, JavaScript
- Source: v8.dev
Senior questions
1. Under load p99 grew from 40 ms to 4 s, one core is at 100%, the health check sometimes fails, and the database and external APIs are fast. The handler has
regex.test(req.body.query)with nested quantifiers and aJSON.parseof a large file. What is happening, how do you prove it in production, and how do you fix it?Answer
This is event loop blocking: synchronous work in the single JS thread holds all requests, even the health check. A regular expression like
/^(a+)+$/on a specially chosen string causes catastrophic backtracking (ReDoS), and the input is controlled by the client.JSON.parseof a large file is synchronous too. Proof:perf_hooks.monitorEventLoopDelayandeventLoopUtilizationshow loop delay and saturation, andnode --cpu-profpoints to the exact function. The fix: limit input length, remove nested quantifiers or use an engine without backtracking, parse once with a cache, stream it, or move it to a worker.Follow-up question: What event loop delay threshold would you set in an alert, and why per instance rather than an average?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
2. A recursive function schedules itself through
process.nextTicka million times, and asetTimeout(..., 0)is set before it. When does the timer fire? What changes withsetImmediate, and why is this dangerous on a server?Answer
The timer fires only after all million ticks. The
nextTickqueue is drained completely before the loop moves on, and new ticks land in the same queue. This is starvation: nothing is explicitly blocked, but timers and I/O get no time. WithsetImmediateevery iteration goes to the next loop turn, so the timer fires almost immediately. On a server such a loop stops processing all requests, which is why long work is split into chunks withsetImmediate, not withnextTickor a promise chain.Follow-up question: Does the problem go away if the recursion is done through
Promise.resolve().then(...)?- Knowledge base: Node.js
- Source: nodejs.org
3. A Readable stream with
.pipe(dst)fails with an error. What happens todst, and what is the consequence on a real server? How do you fix it?Answer
.pipe()does not forward the error and does not close the destination: the docs say plainly that when a Readable errors, the Writable is not closed automatically. On a server this means open file descriptors or an HTTP response that never finishes, and the client hangs until the timeout. The fix isstream.pipeline(preferably thenode:stream/promisesversion): it destroys all streams in the chain and rejects the promise with the error. You can then handle the error in one place, withtry/catcharoundawait pipeline(...).Follow-up question: How do you cancel a pipeline if the client closed the connection mid-response?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
4. You need an endpoint that exports 5 million rows from Postgres to CSV. The current code does
findAll,mapandres.send, and the process dies with OOM. Design a solution. What happens if the client is slow or closes the tab?Answer
Rows are read in batches: a server-side cursor or a stream such as
pg-query-stream. Then you build the chainpipeline(dbStream, csvTransform, gzip, res). When the client reads slowly,res.writereturnsfalse, the pipeline pauses the whole chain, and the cursor fetches the next batch only after the previous one is consumed, so memory stays roughly constant. If the client closes the tab, the pipeline destroys the streams, and that must close the cursor and return the connection to the pool, which you should verify with a test. For very large or long exports a background job is better: a file in storage and a link for the client.Follow-up question: How long can you hold a transaction with a cursor on a large table, and how does it hurt
VACUUM?- Course: Node.js: getting started
- Knowledge base: Node.js, PostgreSQL
- Source: nodejs.org
5. An API has an endpoint that generates a PDF report and computes statistics: 2 seconds of CPU per request, and other endpoints slow down under load. Design a solution. What happens when there are twice as many requests as the workers can handle?
Answer
The main thread must not compute on its own. You take a
worker_threadspool of fixed size, roughly the number of cores minus one, because creating a Worker per request is expensive; a ready option is the piscina library. The key question is overload: you put a bounded queue before the pool, and when it is full you answer 429 or 503 withRetry-Afterinstead of piling up tasks in memory. Large buffers are passed viatransferListwithout copying. If the report can wait, a background queue is better: the client gets 202 and a task status, and workers scale separately.Follow-up question: Why should you not keep an unlimited database connection pool per thread in
worker_threads?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
6. After a deploy
heapUsedslowly grows and OOM happens a few hours later, withMaxListenersExceededWarningin the logs. In the handler aMapcaches entries with the keyid + Date.now(), and every request adds a listener to a global event bus. What is wrong, and how do you confirm the leak on a real service?Answer
The first problem is an unbounded
Map. The key containsDate.now(), so the cache never hits and only grows. You need an LRU with a limit and TTL. The second is a listener per request that is never removed, and the closure holdsuserandres, so the GC cannot collect them.MaxListenersExceededWarningis only a warning in stderr, not a limit. To confirm, take several heap snapshots under load (--inspect,v8.writeHeapSnapshot,--heapsnapshot-signal) and compare them in Chrome DevTools in Comparison mode, looking at retainers. In production a snapshot pauses the process, so you take it on one instance removed from traffic.Follow-up question: How would a
WeakMaphelp here, and how would it not?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
7. How does the garbage collector in V8 work? Why can a service have pauses of hundreds of milliseconds, and what can you tune?
Answer
V8 splits the heap into generations, because most objects live a short time. New objects go to the new space, which is cleaned often by the minor GC (scavenge): it is fast because only live objects are copied. Survivors move to the old space, where the heavier major GC (mark-sweep-compact) works, although V8 does a lot of that work in parallel or incrementally. The pauses show up as event loop delay. Typical causes of long pauses: the heap is near its limit and the GC almost never stops, or there are objects of "middle" age such as a cache. Tuning:
--max-old-space-sizefor the container size,--max-semi-space-size, fewer allocations in hot code. You measure with--trace-gcorPerformanceObserverwithgc.Follow-up question: What
--max-old-space-sizewould you set in a 1 GB container, and why not exactly 1024?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
8. A colleague suggests just logging the error in
process.on('uncaughtException')so the service "does not crash". What do you answer, and what is the right error handling strategy for a Node service?Answer
That is dangerous: the docs say that resuming normal operation after
uncaughtExceptionis not safe. The exception could have been thrown mid-operation, leaving a transaction open, a resource unreleased or a counter not decremented. The service looks alive but runs in a corrupted state, and such a bug is harder to find than a crash. The right approach: log the error with context, try to finish current work with a hard timeout, and exit with a non-zero code, leaving the restart to the orchestrator. Operational errors (a timeout, a 404 from an API) are handled in code, while bugs (TypeError) are not hidden and must reach Sentry.Follow-up question: How does
uncaughtExceptionMonitordiffer fromuncaughtException?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
9. Describe graceful shutdown of a Node service in Kubernetes that serves HTTP and processes jobs from a queue. What is the order of actions after SIGTERM, and what happens if something hangs?
Answer
Without a SIGTERM handler Node exits immediately and cuts active requests, and after the grace period (30 s by default) Kubernetes sends SIGKILL. The order: first readiness starts returning an error so the load balancer removes traffic, because removal from endpoints and SIGTERM happen in parallel. Then
server.close()stops accepting new connections, and the queue worker stops taking new jobs and waits for active ones. Only at the end do you close the database and Redis pools, because active requests still need them. A hard deadline shorter than the grace period is mandatory, after which the process exits forcibly, and jobs that did not finish are returned by the queue, so they must be idempotent.Follow-up question: How does shutdown differ in Lambda, and why is this code almost unnecessary there?
- Course: Node.js: getting started
- Knowledge base: Node.js, Kubernetes
- Source: kubernetes.io
10. The SIGTERM handler only does
server.close(() => process.exit(0)). During a deploy some requests are cut and some pods are killed with SIGKILL. Why is this not enough, and how do you fix it?Answer
server.close()waits for all active requests to finish, so one hung request holds the callback forever and SIGKILL arrives. Since Node 19,closeitself closes idle keep-alive connections, but active ones remain. There is no deadline: you need a timer (withunref) that after a timeout callsserver.closeAllConnections()and exits with a non-zero code. There is also no pause for readiness, so the load balancer still sends requests to the closed port. First you must take the instance out of balancing, and only then close the server.Follow-up question: What happens to WebSocket connections on
server.close()?- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
11. You need every log line and every SQL query to know the
requestIdandtenantIdwithout passing parameters through all functions. How do you do this withAsyncLocalStorage? What goes into the store, and what are the risks?Answer
Middleware wraps request handling in
als.run(store, next), and the logger and database layer read the context withals.getStore().runis better thanenterWith, because the context is limited to the scope of the call. Only cross-cutting context goes into the store:requestId,tenantId,userId, not business arguments, otherwise the code becomes implicit. Risks: the context is lost in custom pools and libraries that do not preserve async context, and background jobs need their ownrun. It is also a good alternative to Nest request-scoped providers, which are expensive.Follow-up question: How do you pass the context from an HTTP request into a queue job and back into the worker's logs?
- Course: Node.js: getting started
- Knowledge base: Node.js, NestJS
- Source: nodejs.org
12. A Node service sits behind an AWS ALB. At low traffic occasional 502s slip through, there is nothing in the application logs, and CPU is low. What mechanism do you suspect first, and how do you verify it?
Answer
Most likely it is a keep-alive race. Node closes an idle connection after
server.keepAliveTimeout, which is 5 seconds by default, while the ALB has an idle timeout of 60 seconds and at that very moment sends a new request over such a connection. The request never reaches the application, so its logs are empty. The fix: makekeepAliveTimeoutlarger than the load balancer's idle timeout, and raiseheadersTimeouta bit too. You verify it with ALB access logs (target status and timing fields) and by reproducing the request after a pause.Follow-up question: What similar race happens on the Node HTTP client side, and how does your code see it?
- Course: Node.js: getting started
- Knowledge base: Node.js, nginx
- Source: docs.aws.amazon.com
13. What timeouts does a Node.js HTTP server have by default (
keepAliveTimeout,headersTimeout,requestTimeout,server.timeout)? What protection do they give and what not?Answer
keepAliveTimeout(5 s) is the idle time of a keep-alive connection between requests,headersTimeout(60 s) is the time allowed to receive headers,requestTimeout(300 s) is the time for the whole request.server.timeoutis 0 by default, meaning there is no separate socket inactivity timeout. Together they protect against slow clients of the slowloris kind that keep connections open. But they do not limit how long your handler runs: a request hung on the database is not stopped by these timeouts. So the deadline must be passed on to calls to external APIs and the database.Follow-up question: How do you pass the request deadline into an external API call and into a Postgres query?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
14. What scope do NestJS providers have by default, and what does that mean for state in class fields? When is
REQUESTscope needed, what does it cost, and what can you use instead?Answer
By default providers are singletons: one instance per application. So request state in the fields of such a service gets mixed between concurrent requests.
Scope.REQUESTcreates an instance per request but "bubbles up" the dependency chain: everything that depends on it becomes request-scoped too. The cost is extra allocations and slower requests. For request context (id, tenant, user) it is better to useAsyncLocalStorage. A cron job or queue consumer that depends on a request-scoped provider has no HTTP request, and that causes an error.Follow-up question: What happens to a cron job if it depends on a request-scoped provider?
- Knowledge base: NestJS
- Source: docs.nestjs.com
15. A Nest service hashes passwords with async
scrypt, serves files from disk, compresses responses withgzipand calls third-party APIs. At peak, login takes seconds while CPU is not maxed out. What of this goes into the libuv thread pool, and how do you chooseUV_THREADPOOL_SIZE?Answer
The thread pool handles
fsfile operations,dns.lookup, asynccrypto(pbkdf2,scrypt,randomBytes) and asynczlib. Network sockets, pipes anddns.resolvedo not use the pool, they are served by poll through epoll or kqueue. The default pool size is 4, so four longscryptcalls occupy all threads and the remainingfsanddns.lookupcalls wait in the queue. The size is set with theUV_THREADPOOL_SIZEvariable before the pool is first used, assigning it later changes nothing. For CPU tasks more threads than cores will not help, and hashing is better moved to a separate worker thread pool.Follow-up question: How do you notice thread pool saturation in metrics if Node does not export its queue length?
- Course: Node.js: getting started
- Knowledge base: Node.js
- Source: nodejs.org
16. A Node service in Kubernetes sometimes ends as
OOMKilledwith no JS error and sometimes printsJavaScript heap out of memory, and p99 jumps every few minutes. How does Node pick the heap size in a container, and what would you set?Answer
Node sees the cgroup memory limit and by default gives the heap roughly half of it.
OOMKilledwithout a JavaScript error means RSS exceeded the limit because of memory outside the heap:Buffer, native modules, stacks, code space. You set--max-old-space-sizeto roughly 70 to 75% of the limit, or less when there are large buffers. A bigger heap means longer final mark-compact pauses, which shows up in p99. Metrics:PerformanceObserverwith thegctype,monitorEventLoopDelay, andrssandheapUsedseparately.Follow-up question: Why can two 1 GB pods have a better p99 than one 2 GB pod with the same traffic?
- Course: Node.js: getting started
- Knowledge base: Node.js, Kubernetes
- Source: nodejs.org
17. A NestJS latency metrics interceptor always logs
took 0ms: it callsnext.handle()and logs the difference betweenDate.now()andstartright away, before the result is returned. What is wrong, how do you rewrite it, and which requests will it not see?Answer
next.handle()returns a lazy Observable: the handler runs only on subscription, that is afterintercepthas already returned. So the time difference is almost zero. You must measure inside the stream:next.handle().pipe(tap(...)), and for errors add error handling orfinalize. The error status is set later by the exception filter, so you take it from the error itself. Guards run before interceptors, so 401 and 403 responses will not reach this metric. For the full picture you add middleware withres.on('finish').Follow-up question: How do you add a 5 second timeout to the handler in an interceptor, and what happens to the work the handler already started?
- Knowledge base: NestJS
- Source: docs.nestjs.com
Train these questions
Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.
Answer out loud and get graded
Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.
The app is coming to the stores soon.
Other topics
- JavaScript interview questions and answers
- TypeScript interview questions and answers
- React interview questions and answers
- Frontend performance interview questions and answers
- HTTP interview questions and answers
- SQL interview questions and answers
- Databases interview questions and answers
- Git interview questions and answers
- Algorithms interview questions and answers
- System design interview questions and answers
- DevOps interview questions and answers
- Web security interview questions and answers
- Testing interview questions and answers