48 questions

Node.js interview questions and answers

Junior questions

  1. 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?

  2. 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?

  3. 3. How do CommonJS and ES modules differ in Node.js, and how does Node decide which format a file uses?

    Answer

    CommonJS uses require and module.exports, ES modules use import and export. Node treats a file as an ES module if it has the .mjs extension or the nearest package.json has "type": "module". Otherwise it is CommonJS. ES modules load asynchronously and support top-level await, while CommonJS has __dirname and __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?

  4. 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.env instead of being hardcoded. A .env file is handy locally, but it goes into .gitignore and 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 in process.env are 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?

  5. 5. In NestJS with no custom filters, a controller throws a NotFoundException, a plain Error with text about a database password, and an HttpException with an object and status 429. What does the client get in each case?

    Answer

    NotFoundException gives 404 with a body containing statusCode, message and error. Nest treats a plain Error as unexpected: the client gets 500 and a generic Internal server error, and the error text goes only to the log, not the body. HttpException with 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?

  6. 6. How do you add Swagger (OpenAPI) to NestJS, and how do you avoid writing @ApiProperty on every DTO field?

    Answer

    The @nestjs/swagger package is set up in main.ts: DocumentBuilder assembles the description, SwaggerModule.createDocument builds the document, and SwaggerModule.setup serves the UI. DTO fields are described with @ApiProperty. To avoid writing it by hand, you enable the CLI plugin in nest-cli.json: it adds annotations at build time from TypeScript types and comments. For derived DTOs, helpers like PartialType must 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?

Middle questions

  1. 1. Name the phases of the Node.js event loop in order and say what runs in each. Where do process.nextTick and 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.nextTick and promises are not phases. After every callback Node first drains the nextTick queue, then the microtask queue, and only then moves on. That is why an endless chain of nextTick calls stops the whole loop.

    Follow-up question: Why do these queues run after every single timer rather than after the whole timers phase?

  2. 2. A CommonJS file calls setTimeout(..., 0), setImmediate, process.nextTick, Promise.resolve().then and a synchronous console.log. What is the output order, and which part of it is guaranteed?

    Answer

    The first three are guaranteed: synchronous code first, then the nextTick queue, then microtasks, that is the promise. The order of setTimeout and setImmediate from 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, otherwise setImmediate does. Inside an I/O callback the order is deterministic: setImmediate always runs before the timer.

    Follow-up question: How can you make the order of the timer and setImmediate deterministic without changing the calls themselves?

  3. 3. What does the false returned by write() on a Writable stream mean, and what happens if you ignore it?

    Answer

    write() returns false when the internal buffer has reached highWaterMark. No data is lost: it is a backpressure signal telling you to stop writing and wait for the drain event. If you ignore it, a fast source keeps writing into memory, the buffer grows, and the process can crash with OOM. pipeline does this pausing automatically. The default highWaterMark is 64 KB for byte streams and 16 objects in object mode.

    Follow-up question: How does this work when the source is a for await over a database cursor instead of pipe?

  4. 4. Compare cluster, child_process and worker_threads. What would you choose for a CPU-heavy task, and what for using all cores with an HTTP server?

    Answer

    cluster starts separate processes that share one port, which is how an HTTP server uses all cores, and memory is not shared between processes. worker_threads are 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_process is for running a separate program or isolating crashes. In Kubernetes cluster is 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?

  5. 5. Why does --max-old-space-size not protect a Node.js process from a memory blowup when a large Buffer is allocated? Which memory metrics should you watch?

    Answer

    The contents of a Buffer live outside the V8 heap, and only a small wrapper object lands in the heap. So heapUsed barely changes while arrayBuffers and rss grow. The --max-old-space-size flag 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 watch rss, heapUsed, external and arrayBuffers together.

    Follow-up question: RSS keeps growing while heapUsed is stable. Where do you look next?

  6. 6. Inside try/catch you call [1, 2, 3].forEach(async (id) => { await save(id); }) and save(2) throws. Why does catch not fire, and how do you process the array correctly, sequentially and in parallel?

    Answer

    forEach calls the callback and discards the returned promise, so the function waits for nothing: done is printed before the error, and the rejection becomes an unhandledRejection. This is a typical cause of a worker that finished "successfully" while some records were not saved. Sequentially you process with for...of and await, in parallel with await 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, use Promise.allSettled.

    Follow-up question: Promise.all rejected on the first error. What happens to the remaining requests?

  7. 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 maxSockets is 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, ECONNRESET is possible, so you may automatically retry only idempotent requests. The built-in fetch runs on its own pool (undici), and http.Agent settings do not affect it.

    Follow-up question: Where do you put a circuit breaker and how does it relate to timeouts?

  8. 8. In CommonJS, module a does require('./b') and b does require('./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 b asks for a, it receives the existing but unfinished exports object of a, not a second run. That is why there is no loop, and b sees only what a managed to export before calling require. 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 ReferenceError there?

  9. 9. A module exports a counter n and a function inc that does n++. What does the importer see after calling inc() in CommonJS, and what in ES modules? Why the difference?

    Answer

    In CommonJS module.exports is a plain object, and the value of n is copied into it when it is created. After inc() the destructured variable stays 0. In ESM imports are live bindings: the imported variable points to the variable in the exporting module, so after inc() we see 1. An importer cannot reassign the imported variable, only the exporter itself can change it.

    Follow-up question: Why is there no __dirname in ESM and what replaces it?

  10. 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 ExecutionContext and the route metadata, which middleware does not know. Validation is done by a pipe, for example ValidationPipe with whitelist. 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 via useGlobalGuards(new ...) in main.ts does not go through DI, so its dependencies are not injected. The correct way is to register it as a provider with the APP_GUARD token in a module.

    Follow-up question: How do you make a route public when there is a global guard?

  11. 11. In NestJS, UsersService depends on MailService, which is declared in providers of MailModule, and UsersModule has imports: [MailModule]. The app does not start with Nest 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 entry imports: [MailModule] gives access only to what that module exports, and MailModule exports nothing. That is why UnknownDependenciesException appears. The one-line fix is to add exports: [MailService] to MailModule. Duplicating MailService in the providers of UsersModule is not advisable, because that creates a second instance.

    Follow-up question: What is a dynamic module, and when would you make a module @Global()?

  12. 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 }. useValue gives a ready value or a mock, for example in tests. useClass lets you swap the implementation depending on the environment. useFactory creates a provider with logic and dependencies via inject and can be asynchronous, for example to connect to a database. useExisting creates 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 useFactory returns a Promise?

  13. 13. In NestJS, CheckoutService depends on the interface PaymentGateway, and providers contains the class LiqPayGateway that implements it. Startup fails with Nest 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: a Symbol, 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?

  14. 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 onModuleInit and onApplicationBootstrap are called at listen or init. Startup order: constructor, onModuleInit, onApplicationBootstrap. Shutdown goes: onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown. An async onModuleInit delays startup, because Nest waits for its Promise. That is why connections are opened in onModuleInit and not in the constructor, which cannot be async. System signals work only after enableShutdownHooks().

    Follow-up question: Are these hooks called for a request-scoped class?

  15. 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 in tap or map on 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?

  16. 16. What do the whitelist, forbidNonWhitelisted and transform options do in the NestJS ValidationPipe? What happens to an extra role field in the body and to page=2 in the query?

    Answer

    With whitelist: true the pipe silently strips fields that have no validation decorators, so the extra role in the body disappears. forbidNonWhitelisted: true returns 400 saying the property should not exist instead of stripping it. With transform: true the pipe turns a plain object into an instance of the DTO class, and with @Type(() => Number) the string page=2 from the query becomes a number. Without whitelist extra 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?

  17. 17. ValidationPipe is enabled, but POST /orders with items: [{ sku: 5, qty: -1 }] returns 201. CreateOrderDto has only @IsArray(). Why did the ItemDto rules not fire, and what should you add?

    Answer

    @IsArray() only checks that the value is an array and does not look inside the elements. The ItemDto[] 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) from class-transformer so the elements become class instances with metadata. Without @Type the 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?

  18. 18. How do you organize configuration in NestJS so the app does not start without DATABASE_URL instead of failing on the first request?

    Answer

    You add ConfigModule.forRoot({ isGlobal: true }) and pass a validationSchema (for example Joi) or a validate function: environment variables are checked at startup and the app fails right away with a clear error. In code you use ConfigService.getOrThrow, which throws if the value is missing. With registerAs the config is grouped into a namespace. Note that registerAs validates nothing by itself, the check is still done by the schema or validate.

    Follow-up question: How do you pass the config to TypeOrmModule.forRootAsync and change a value in a test?

  19. 19. Walk a request through authentication with Passport and JWT in NestJS, from login to req.user in 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 route AuthGuard('jwt') runs JwtStrategy: it takes the Bearer token from the header and verifies the signature and exp. Then validate(payload) is called and its result becomes req.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?

  20. 20. All endpoints of a NestJS API must require a JWT by default, except /health and /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_GUARD provider instead of @UseGuards on every controller, so protection applies by default. Exceptions are marked with a custom @Public() decorator that sets a flag via SetMetadata. The guard reads it with Reflector.getAllAndOverride on 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?

  21. 21. What is a dynamic module in NestJS? How do forRoot, forFeature and register differ, and when is @Global() appropriate?

    Answer

    A dynamic module is a module that returns a DynamicModule with providers and exports configured at import time. forRoot is called once at the root for shared configuration, forFeature is used in a feature that relies on an entity already configured by the root, and register gives a separately configured instance for each import. forRootAsync with useFactory and inject is needed when the config depends on other providers, for example ConfigService. @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?

  22. 22. OrdersModule imports UsersModule, and UsersModule already imported OrdersModule, and NestJS crashes at startup with an error about undefined in imports. 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 class undefined at the moment the decorator is applied. The quick fix is forwardRef(() => 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 like madge --circular.

    Follow-up question: Does the caller of emit learn about an error in an async EventEmitter2 listener?

  23. 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 manager given by the callback: manager.decrement(...), manager.save(...). Rollback undoes only what went through that manager. In Prisma the rule is the same: queries go through tx in $transaction(async (tx) => ...), not through the global client.

    Follow-up question: How do you pass the transaction through several services without dragging manager into every method?

  24. 24. Services throw domain errors OrderNotFound and InsufficientFunds, and Prisma throws P2002 on 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: OrderNotFound to 404, InsufficientFunds to 422 or 409, Prisma P2002 to 409, P2025 to 404. The response body has a uniform format, for example Problem Details with a requestId. Unknown errors are returned as 500 with a generic text, and the full stack trace goes only to the log. The filter is registered via APP_FILTER so 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?

  25. 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?

Senior questions

  1. 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 a JSON.parse of 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.parse of a large file is synchronous too. Proof: perf_hooks.monitorEventLoopDelay and eventLoopUtilization show loop delay and saturation, and node --cpu-prof points 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?

  2. 2. A recursive function schedules itself through process.nextTick a million times, and a setTimeout(..., 0) is set before it. When does the timer fire? What changes with setImmediate, and why is this dangerous on a server?

    Answer

    The timer fires only after all million ticks. The nextTick queue 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. With setImmediate every 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 with setImmediate, not with nextTick or a promise chain.

    Follow-up question: Does the problem go away if the recursion is done through Promise.resolve().then(...)?

  3. 3. A Readable stream with .pipe(dst) fails with an error. What happens to dst, 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 is stream.pipeline (preferably the node:stream/promises version): it destroys all streams in the chain and rejects the promise with the error. You can then handle the error in one place, with try/catch around await pipeline(...).

    Follow-up question: How do you cancel a pipeline if the client closed the connection mid-response?

  4. 4. You need an endpoint that exports 5 million rows from Postgres to CSV. The current code does findAll, map and res.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 chain pipeline(dbStream, csvTransform, gzip, res). When the client reads slowly, res.write returns false, 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?

  5. 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_threads pool 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 with Retry-After instead of piling up tasks in memory. Large buffers are passed via transferList without 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?

  6. 6. After a deploy heapUsed slowly grows and OOM happens a few hours later, with MaxListenersExceededWarning in the logs. In the handler a Map caches entries with the key id + 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 contains Date.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 holds user and res, so the GC cannot collect them. MaxListenersExceededWarning is 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 WeakMap help here, and how would it not?

  7. 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-size for the container size, --max-semi-space-size, fewer allocations in hot code. You measure with --trace-gc or PerformanceObserver with gc.

    Follow-up question: What --max-old-space-size would you set in a 1 GB container, and why not exactly 1024?

  8. 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 uncaughtException is 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 uncaughtExceptionMonitor differ from uncaughtException?

  9. 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?

  10. 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, close itself closes idle keep-alive connections, but active ones remain. There is no deadline: you need a timer (with unref) that after a timeout calls server.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()?

  11. 11. You need every log line and every SQL query to know the requestId and tenantId without passing parameters through all functions. How do you do this with AsyncLocalStorage? 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 with als.getStore(). run is better than enterWith, 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 own run. 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?

  12. 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: make keepAliveTimeout larger than the load balancer's idle timeout, and raise headersTimeout a 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?

  13. 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.timeout is 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?

  14. 14. What scope do NestJS providers have by default, and what does that mean for state in class fields? When is REQUEST scope 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.REQUEST creates 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 use AsyncLocalStorage. 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?

  15. 15. A Nest service hashes passwords with async scrypt, serves files from disk, compresses responses with gzip and 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 choose UV_THREADPOOL_SIZE?

    Answer

    The thread pool handles fs file operations, dns.lookup, async crypto (pbkdf2, scrypt, randomBytes) and async zlib. Network sockets, pipes and dns.resolve do not use the pool, they are served by poll through epoll or kqueue. The default pool size is 4, so four long scrypt calls occupy all threads and the remaining fs and dns.lookup calls wait in the queue. The size is set with the UV_THREADPOOL_SIZE variable 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?

  16. 16. A Node service in Kubernetes sometimes ends as OOMKilled with no JS error and sometimes prints JavaScript 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. OOMKilled without 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-size to 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: PerformanceObserver with the gc type, monitorEventLoopDelay, and rss and heapUsed separately.

    Follow-up question: Why can two 1 GB pods have a better p99 than one 2 GB pod with the same traffic?

  17. 17. A NestJS latency metrics interceptor always logs took 0ms: it calls next.handle() and logs the difference between Date.now() and start right 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 after intercept has 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 or finalize. 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 with res.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?

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