2 Answers
2

Handling the requests synchronously on an event based server leaves you open to easy DOS, and certainly has a very negative impact on performance. Regardless of what type of server you use, a robust authentication system should provide some protection against brute force scanning.

The most common practice I've seen for protecting against brute force scanning is to disable an account after a certain number of failed logins - which, IMHO, is a DOS itself. A more practical approach is that implemented by fail2ban - after a certain number of failed attempts from a particular remote address, that address gets a temporary ban at the firewall.

The solution I've used in the past is to implement short queues for mutexes on the username and source IP address - the authentication is not tested until a request has both mutexes. If either queue is full then the request is failed immediately. This still provides some scope for DOS, but its scope should be far better contained.

When dealing with synchronism, one good way of thinking is about state. State is whatever must be retained in some data storage (which can be RAM, disk,... whatever, and not necessarily on the server) so that processing may ultimately proceed and succeed.

When doing synchronous calls (generically, not specifically with Node.js), the caller is an execution thread on some machine, and the "state" is what that execution thread "knows": its current position in the code, its local variables, its call stack. The caller retains that state in RAM for the duration of the call; that's the definition of a synchronous call: while the call is not finished, the caller is blocked. Details may vary depending on the programming language and how it is interpreted/compiled, but for most languages, each execution thread (a language concept) is mapped on a system thread (an operating system concept), which means that the state includes an entry in the system table of threads, a bit of space in the scheduler structures, and the thread stack.

Each system thread has its own stack, allocated when the thread was created. Default size for this stack depends on the OS, and might be programmatically adjusted. On Linux, a thread stack has size 1 MB by default. That's 1 MB of address space, not of RAM: actual pages are allocated when they are accessed, so a typical thread will use only a few dozen kilobytes of "true RAM". Though details vary a lot depending on context, the bottom-line is the following: in a typical application, you can normally handle a few hundred threads at any one time -- which means that your server can host a few hundred ongoing synchronous calls simultaneously. Beyond that, you must think.

Asynchronous calls remove this thread-based state management: the caller regains control immediately and can thus do something else or even exit. However, you must still keep some state somewhere, if only to remember that there is an ongoing call and to know what should be done with the result. The point of going asynchronous is to make manual, fine-grained management of this state, which can be as small as a 16-byte session ID or some similar core state, thus allowing for a lot more simultaneous calls. Depending on context, you might even be able to offload state on the client (which implies extra security issues to deal with, of course). However, implementation becomes a great deal more complex when going asynchronous, and complexity usually comes with extra work, extra bugs, and thus extra vulnerabilities.

So going asynchronous is an optimization, and thus should be envisioned only if the situation makes it worth the extra complexity and effort. As with all performance things, such optimizations should be delayed until an actual serious performance-related problem has been duly noticed, monitored and measured. "Premature optimization is the root of all evil."

Therefore I suggest that your first write all your code synchronously. Then, and only then, will realistic performance tests tell you whether synchronous calls will be enough or not; and if going asynchronous is indeed warranted, at that point you will know a lot more about your own application, making you ready (or at least readier) to tackle the inherent complexity of asynchronous programming.