How I Discovered the Cause of Slowness in My Express App

Please note that this is a back-ported post from 2016.

Recently I discovered some slowness in my express app.

A bit background here: in the platform we are building here in Madadata, we are using an external service to provide user authentication and registration. But in order to test against its API without incurring the pain of requesting from California (where our CI servers are) to Shanghai (where our service providers' servers are), I wrote a simple fake version of their API service using Express and Mongoose.

We didn't realize the latency of my service until our recently started load testing, where it shows that more than half of requests didn't return within 1 second and thus failing the load test. As a simple Express app using Mongoose there is hardly any chance of getting it wrong, at least not anywhere near 1 second of latency.


The screenshot above for running mocha test locally revealed that there is indeed some problem with the API service!

What went wrong?

From the screenshot I can tell that not all APIs are slow: the one where users log out and also the one showing current profile is reasonably fast. Also, judging from the dev logs that I printed out using morgan, for the slow APIs, their response time collected by Express is indeed showing a consistent level of slowness, (i.e. for the red flagged ones, you are seeing a roughly sum of latency of two requests above them, respectively).

This actually rules out the possibility that the slowness comes from connection, rather than within Express. So my next step is to look at my Express app. (N.B. this is actually something worth ruling out first, and I personally suggest trying one or two other tools rather than mocha, e.g. curl and even nc before moving on, because they almost always prove to be more reliable than the test code you wrote).

Inside Express

Express is a great framework when it comes to web server in Node and it has come a long way in terms of speed and reliability. I thought it is more likely due to the plugins and middlewares that I used with Express.

In order to use MongoDB as session store I used connect-mongo for backing my express-session. I also used the same MongoDB instance as my primary credential and profile store (because why not? it is a service for CI testing after all). For that I used Mongoose for ODM.

At first I suspected that it might be because of the built-in Promise library shipped by default in Mongoose. But after changing it with ES6 built-in one the problem wasn't solved.

Then I figured it is worth to check the schema serialization and validation part. There is only one model and it is fairly simple and straightforward:

const mongoose = require('mongoose') const Schema = mongoose.Schema const isEmail = require('validator/lib/isEmail') const isNumeric = require('validator/lib/isNumeric') const passportLocalMongoose = require('passport-local-mongoose') mongoose.Promise = Promise const User = new Schema({ email: { type: String, required: true, validate: { validator: isEmail }, message: '{VALUE} 不是一个合法的 email 地址' }, phone: { type: String, required: true, validate: { validator: isNumeric } }, emailVerified: { type: Boolean, default: false }, mobilePhoneVerified: { type: Boolean, default: false }, turbineUserId: { type: String } }, { timestamps: true }) User.virtual('objectId').get(function () { return this._id }) const fields = { objectId: 1, username: 1, email: 1, phone: 1, turbineUserId: 1 } User.plugin(passportLocalMongoose, { usernameField: 'username', usernameUnique: true, usernameQueryFields: ['objectId', 'email'], selectFields: fields }) module.exports = mongoose.model('User', User)

Mongoose Hooks

Mongoose has this nice feature where you can use pre- and post- hooks to interact and investigate document validation and saving process.

Using console.time and console.timeEnd we can actually measure the time spent during these processes.

User.pre('init', function (next) { console.time('init') next() }) User.pre('validate', function (next) { console.time('validate') next() }) User.pre('save', function (next) { console.time('save') next() }) User.pre('remove', function (next) { console.time('remove') next() })'init', function () { console.timeEnd('init') })'validate', function () { console.timeEnd('validate') })'save', function () { console.timeEnd('save') })'remove', function () { console.timeEnd('remove') })

and then we are getting this more detailed information in mocha run:


Apparently document validation and saving doesn't take up large chunks of latency at all. It also rules out the likelihood a) that the slowness comes from connection problem between our Express app and MongoDB server, or b) that the MongoDB server itself is running slow.

Passport + Mongoose

Turning my focus away from Mongoose itself, I start to look at the passport plugin that I used: passport-local-mongoose.

The name is a big long but it basically tells what it does. It adapts Mongoose as a local strategy for passport, which does session management and registering and login boilerplate.

The library is fairly small and simple, so I start to directly edit the index.js file within my node_modules/folder. Since function #register(user, password, cb) calls function #setPassword(password, cb), i.e. specifically this line, I started to focus on the latter. After adding some more console.time and console.timeEnd I confirmed that the latency is mostly due to this function call:

pbkdf2(password, salt, function(pbkdf2Err, hashRaw) { // omit }


The name itself suggested that it is a call to cryptography library. And a second look at the README show that the library is using 25,000 iterations.

Like bcryptpbkdf2 is also a slow hashing algorithm, meaning that it is intended to be slow, and that slowness is adjustable given number of iterations, in order to adapt against ever-increasing computation power. This concept is called key stretching.

As written in the Wiki, the initial proposed iteration number was 1,000 when it first came out, and some recent updates on this number reached as hight as 100,000. So in fact the default 25,000 was reasonable.

After reducing the iterations to 1,000, my mocha test output now looks like:


and finally it is much acceptable in terms of latency and security, for a test application after all! N.B. I did this change for my testing app, it does not mean your production app should decrease the iterations. Also, setting it too high will also render the app vulnerable to DoS attack.

Final thoughts

I thought it would be meaningful to share some of my debugging experience on this, and I'm glad that it wasn't due to an actual bug (right, a feature in disguise).

Another point worth mentioning is that for developers who are not experts on computer security or cryptography, it is usually a good idea not to homemake some code related to session/key/token management. Using good open source libraries like passport to start with is a better idea.

And as always, you'll never know what kind of rabbit hole you'll run into while debugging a web server - this is really the fun part of it! 


Please note that this is a back-ported post from 2016.

This post is a translation of Julia Evans’ How to ask good questions. I personally like it a lot and think it’ll be a good idea to share with translation. She has acknowledged the translation but not endorsed or verified it.

这篇文章是 Julia Evans 的这篇博客文章的翻译。我觉得写得很不错所以翻译了这个版本。翻译得到了原作者授权,但没有审核或者背书。


首先,我其实非常同意「你完全可以问很蠢或者不那么优秀的问题」。我自己就经常问别人比较蠢的问题,那些问题其实我自己可以 Google 或者搜索代码库就可以解决。虽然我大多数时候尽量避免,但是有时候还是问了很傻的问题,之后也不觉得是世界末日。







  1. 陈述关于这个话题你目前的理解
  2. 然后问「我理解的对么」?

例如,我最近和一个人(一个非常优秀的提问者)讨论计算机网络!他说:「所以我的理解是,这里有一系列的递归 DNS 服务器……」。但那不对!事实上,递归 DNS 服务器是没有「一系列」的(当你访问递归 DNS 服务器的时候,这里只有一个递归服务器参与其中)。所以他事先陈述了他的目前的理解,这帮助我们很容易的就厘清了其中的工作原理。

我之前对 rkt 感兴趣,但并不理解为什么 rkt 跑容器的时候比 Docker 多占用了那么多磁盘空间。

但「为什么 rkt 比 Docker 占用更多的磁盘空间」感觉上不像该提的问题——我或多或少明白它的代码是如何工作的,但并不明白为什么他们要这么写。所以我向 rkt-dev 邮件组写了这个问题:「为什么 rkt 存储容器镜像的方式不同于 Docker?


  • 写下了我对 rkt 和 Docker 是如何在磁盘上存储容器的理解
  • 提出了几点我认为的他们如此设计的理由
  • 然后就只问了「我的理解对么?」




我一开始的很多提问都有点模糊,比如「SQL 里面 join 是怎么工作的?」那个问题并不好,因为 join 的工作原理分好多部分!对方怎么可能知道我想了解的是哪一部分呢?

我喜欢问的那些问题,他们的答案是一个简单直接的事实。比如,在我们的 SQL join 的例子里面,一些以事实为答案的问题可能是:

  • Join 两个大小为 N 和 M 的表的时间复杂度是什么?是 O(NM)? 还是 O(NlogN) + O(MlogM)?
  • MySQL 是不是总是在 join 之前先对 join 的列进行排序?
  • 我知道 Hadoop 有时候会做 hash join——其他数据库引擎是不是也会使用这个?
  • 当我在一个有索引的列和一个未索引的列上做 join 的时候,我需要事先对未索引的列排序么?

当我问这样超级具体的问题的时候,对方不总是知道答案(但这没关系!!),但是至少他们明白我想了解哪种问题——比如,明显我对如何使用 join 不感兴趣,我想了解的是具体实现和算法。


当有人给我解释一个东西的时候,他们常常会说一些我不明白的东西。比如,有人给我解释数据库的时候可能会说:「好,我们在 MySQL 里面使用了乐观锁,然后……」我完全不知道「乐观锁」是什么。所以,那就是一个理想的提问时机!:-)





刚开始现在这份工作的时候我在数据组。在了解我的新工作职责的时候,里面全是这种词!Hadoop、Scalding、Hive、Impala、HDFS、Zoolander 等等。我之前可能听过 Hadoop,但基本上不知道以上任何单词的意思。里面有的词是内部的项目,有的词是开源项目。所以我就开始请别人帮我了解其中每一个概念的含义以及他们之间的关系。期间我可能问过这样的问题:

  • HDFS 是一个数据库么?(不是的,它是一个分布式文件系统)
  • Scalding 用到了 Hadoop 么?(是的)
  • Hive 用到了 Scalding 么?(没有)



当我打出上面那些 SQL 问题的时候,我在 Google 里面搜索了「如何实现 SQL 的 join 语句」。点击其中一些链接之后我看到了「噢,我明白了,有时候有排序,有些时候有哈希 join,我都听过」,然后写下了我的一些更具体的问题。一开始先自己 Google 一番帮我提出一些稍稍更好一点的问题。

话虽如此,我觉得有的人太坚持「永远不要在自己 Google 之前提问」这件事——有时候我和别人吃午饭时候,好奇对方的工作内容,我会问一些比较基础的问题。这完全没问题!





  • 对对方来讲这是一个好的时机么?(如果对方正在处理一个紧迫的事情,很可能不是)
  • 我问这个问题节约的时间是不是值得我提问所花的时间?(如果我提问需要5分钟,可以节约我2小时的时间,那太棒了 :D)
  • 对方需要花多少时间来回答我的问题?(如果我有个需要半小时时间的问题,我可以和对方预约一个之后的一整块时间;如果我只是有个小的问题,可能我就会立刻就问)
  • 对方是不是在这个问题上太资深了?我觉得总是问那些对某个话题最在行和资深的人,是挺容易的陷进去的一个误区。但是常常去找那些稍微不那么资深的人会更好——他们常常可以回答你的大多数问题,回答问题的压力也分散了,而且他们还可以有机会展示他们的知识(这一点很棒)。



ESR (Eric Steven Raymond)写的「如何聪明的问问题」是一篇流行但挺刻薄的文章(它开头就有「我们叫这样的人怂货」这样的糟糕语句)。这是关于在互联网上向陌生人提问的。在网上向陌生人提问是一个超级有用的技能,也可以给你很有用的信息,但它也是提问的「困难模式」。你的提问对象不了解你的处境,所以你需要以成倍的耐心去陈述你想了解什么。我并不喜欢 ESR 的那篇文章,但是它讲了一些有价值的东西。其中「如何有建设性地回答问题」一章其实非常棒。



Etsy 的 Debriefing facilitation guide 其中的「提问的艺术」一章是在讨论突发事件的背景下,对此的一个精彩介绍。以下是来自其中的一些问题:

当你怀疑这类失败发生的时候你会寻找什么迹象? 你怎么判断一个「正常」的情况? 你怎么知道数据库下线了? 你怎么知道你需要报告给哪个组?



André Arko 的如何为开源软件做贡献文章里面,我最喜欢的一部分是:

现在你读完了所以所有的 issues 和 pull requests,开始寻找你可以回答的问题了。用不了多久你就会发现有人在问之前回答过、或者在你刚刚读过的文章里面回答过的问题。回答这些你可以回答的问题。



好的问题也可以是对社区做的一大贡献!我之前在 Twitter 问了一堆关于 CDN 的问题,然后在我的文章里面写出了总结。很多人告诉我他们很喜欢那篇文章,我觉得我提那些问题不只是帮到了我,也帮到了许多人。



Please note that this is a back-ported post from 2016.

今天我们的前端工程师告诉我说,postcss 的一个小 bug/feature 花了他很多时间才搞清楚。

Postcss 是我们用的一个处理 CSS 的前端框架,和我们目前也在用的 webpack 可以一起使用。后者负责把各种 JavaScript,CSS 以及图片等资源进行打包,而前者的主要工作就是在此之前把 CSS 代码进行一遍预处理。

比如 Webpack 的配置文件里面可能有这么一段:

// webpack.config.js module.exports = { // 省略 postcss: [require("autoprefixer"), require("precss")] // 省略 };

其中的 postcss 部分是一个数组(array),里面的每一个元素都是一个 postcss 的插件。Postcss 自己做的非常模块化,大部分任务都交给各式各样的插件来完成,通过组合不同的插件来实现多种功能。「Do one thing and do one thing well」,多么 UNIX 的哲学。

但我们的前端工程师遇到问题是,他之前不知道这个定义的顺序是有意义的,还以为只要把所需的功能模块放上去就好了。我说,其实这个和 UNIX 的 pipe 是一样的,或者你如果用过 gulp 也更能清楚这种流式处理(Streamline/Pipeline)系统的工作原理:顺序其实非常重要。他总结说是自己只用过 webpack 的原因。但是我仔细一想,其实这种情形我自己倒是反复遇到过。


记得本科的时候读过 Joel Spolsky 的文章 The Law of Leaky Abstractions,大体讲的是,所有的抽象层都会有漏洞,总会出现一个情况下,你必须绕过抽象层去了解背后的细节。文中举了 TCP 作例子,说很多程序都依赖 TCP 协议,把易错的 IP 层协议封装好,让你发送的数据总是有序、完整和没有冗余的。但是总会出现一些情况,比如老鼠咬断了网线,那时候你还是会观察到抽象层的破碎(TCP 不再正常工作),不得不处理那些意想不到的情况。

抽象是一个超级强大的工具,事实上有句很有名的话就是说:「计算机科学里面,没有什么问题是多加一层抽象不可以解决的」。比如上面 webpack 的配置文件其实就是对流式代码的一种封装和抽象。而且抽象其实不只是计算机科学里面的概念。它是一种人类最最基本的思维工具,可以帮助人脑减少同时要处理的信息量,把不重要的、可重复的细节滤掉,而着眼于目前最重要的东西。抽象大部分情况下都是思维的利器,也是每一个程序员必须掌握的本领。



最近有个好朋友在做零基础编程培训,课程设计的紧凑但很有含金量:他会教所有学员在三周时间内,先学会用 HTML 搭建一个静态网页,然后学习基本的 CSS 知识,用 Twitter Bootstrap 美化页面,最后学习 JavaScript 给页面加上一些动态的元素。


的确,现在的很多程序语言(比如 Python,JavaScript,Swift)已经比以前(比如 C,C++)好学太多了,不用手动管理内存,不用学习文件系统,甚至可以拖拽式的操作,完成一个基本的「编程」。我个人非常喜欢这些「user friendly」的编程语言,而且无论是 还是 code academy 都是非常有价值的存在——因为他们都旨在激发大家对编程的兴趣,而编程几乎肯定是未来的必备技能(最多是换一种形式罢了,编程思维才是核心);他们无一例外的都要告诉受众:编程很容易,开始学习吧!



  • 为了保证学员在最短时间内接触最有意思的东西,编程语言用相对好入手的 JavaScript,因为网页编程是最「所见即所得」的编程方法。但是每个人用的浏览器可能不同,所以最好用 Twitter Bootstrap 这样的 CSS 框架,至少整体页面的 look and feel 不会因为浏览器不同而不同——但是,但是,我和你打赌肯定会有人用 IE 6 的,oops!
  • 编程环境也必须得统一,否则就光去解决每个人的配置问题了(想想几种不同的换行符,想想文件编码,想想至少四种不同的 npm 版本)。我提议用 Vagrant,让每个人都用同一个虚拟机镜像,但是这个方案因为中国的网速最后作罢。(不过好在他最后直接找到了 codepen 这一个终极的受控的环境。)
  • 为了让大家不受网络连接的问题影响,需要找到靠谱的代理或者 VPN,否则一些 CDN 可能会受影响,也会出现比如 CSS 无法加载的问题,而如何教会大家使用网络工具本身又是一个挑战

你看,虽然已经是一个及其简化的配置(所见即所得,不需要编译),一个极其容错的编程平台(浏览器),也还是到处都是「有漏洞的抽象」。如果一个人因此认为自己掌握了前端编程而信心满满的时候,他下次换个机器,换个浏览器,甚至哪怕忘记打 </script> 里面的 / 的时候,就会感觉到受挫和沮丧。



  1. 发现一个很好的框架/抽象/编程语言特性,玩起来发现很有意思,可以提高效率,做到以前不能或者不容易做的事情
  2. 直到你用它来做一些更重要的事情,用到更重要的项目里面,才发现原来里面这么多的「坑」,于是你不得不开始研究它背后的原理,读文档,搜 StackOverflow 的问题,读源代码,用不同的配置方法去调试,直到你搞清楚了它的原理,它究竟解决什么问题,解决这个问题的思路是什么,具体做法是什么
  3. 这个时候再回过头来看,原来它这层抽象是有这么一个意义,而且比它取而代之的之前方法更简洁、高效,只是唯一的弊端是你之前踩过的那些坑,并且你知道了如何避免,甚至改进它


  1. 发现编程很好玩,可以用来写一个简单的程序,可以给朋友炫耀,自己也体会到了动手的快乐
  2. 直到你想深入学习,才发现踩了大坑,你开始去学习文件系统,学习内存管理,学习好的编程习惯,学习数据管理和数据库,学习数据结构,学习离散数学,学习编译器,你才发现这些东西原来都联系在一起,才发现那个「找不到对象」的错误提示背后指的是数据库没有启动,而不是一个悲伤的故事
  3. 这个时候再回过头来看,原来编程和编程思维是这样的,和想象中有那么多的不一样。原来 DeepMind 下围棋赢了人类和强人工智能的到来之间没有那么强的因果关系,也不再会被新闻媒体随随便便的忽悠了;最关键的,是你掌握了一整套分析和解决问题的思维框架

不难发现以上都是 2. 最长,这也的确是最漫长的一个过程,甚至还意味着很多次在 123 之间的反复循环。