Edge Cache
Do you lead or follow? Frequently I observe application level caching designed by, "what other's are doing", without thinking about all available solutions. If your application utilizes things like redis or memcahce and you think that endpoints serving up the cached data are fast, think again. While the de-facto, "monkey see, monkey do" patterns of an application cache layer is a good starting point, you are not a monkey and it's time to branch out.
Out of scope:
In this discussion invalidating cache data is out of scope, as most of the time this is application specific. For example you could have a layer that detects DB changes and then deletes any associated cache keys based on a common pattern, or if data changes are not at the upmost importance it could be as simply as setting a cache key expiration when inserting into the cache.
De-facto Example
Edge/Gw Example
Why?
Why take the additional step of transitioning a local application cache to a gateway layer? Primarily, if you're leveraging OpenResty with LuaJIT, the performance benefits are substantial compared to common application frameworks like Java, Python, Node.js, Ruby, and others. Numerous benchmarks indicate that LuaJIT-optimized code exhibits speed close to C and C++ in many instances.
Performance: The numbers speak for themselves:
Openresty
ab -n 10000 -c 512 http://127.0.0.1:8080/cacheable
Server Hostname: 127.0.0.1
Server Port: 8080
Document Path: /cacheable
Document Length: 14 bytes
Concurrency Level: 512
Time taken for tests: 0.428 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1690000 bytes
HTML transferred: 140000 bytes
Requests per second: 23361.48 [#/sec] (mean)
Time per request: 21.916 [ms] (mean)
Time per request: 0.043 [ms] (mean, across all concurrent requests)
Transfer rate: 3855.56 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 10 3.3 9 27
Processing: 3 12 3.7 11 29
Waiting: 0 8 3.4 7 24
Total: 12 21 3.6 20 34
Percentage of the requests served within a certain time (ms)
50% 20
66% 21
75% 23
80% 25
90% 27
95% 28
98% 32
99% 33
100% 34 (longest request)
Ruby(v3.2.3)
bundle exec puma -w 8 -t 8:32 -p 8081
ab -n 10000 -c 512 http://127.0.0.1:8081/cacheable
Server Hostname: 127.0.0.1
Server Port: 8081
Document Path: /cacheable
Document Length: 13 bytes
Concurrency Level: 512
Time taken for tests: 0.789 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1860000 bytes
HTML transferred: 130000 bytes
Requests per second: 12681.10 [#/sec] (mean)
Time per request: 40.375 [ms] (mean)
Time per request: 0.079 [ms] (mean, across all concurrent requests)
Transfer rate: 2303.40 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 14 3.9 13 30
Processing: 6 26 10.1 23 90
Waiting: 1 21 9.6 18 86
Total: 18 39 11.4 36 110
Percentage of the requests served within a certain time (ms)
50% 36
66% 40
75% 43
80% 45
90% 51
95% 61
98% 79
99% 88
100% 110 (longest request)
node.js(v20.11.0):
NODE_ENV=production node raw_nodejs_server.js
ab -n 10000 -c 512 http://127.0.0.1:8082/cacheable
Server Hostname: 127.0.0.1
Server Port: 8082
Document Path: /cacheable
Document Length: 13 bytes
Concurrency Level: 512
Time taken for tests: 0.837 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 880000 bytes
HTML transferred: 130000 bytes
Requests per second: 11946.48 [#/sec] (mean)
Time per request: 42.858 [ms] (mean)
Time per request: 0.084 [ms] (mean, across all concurrent requests)
Transfer rate: 1026.65 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 4.4 0 21
Processing: 20 40 6.1 40 54
Waiting: 1 40 6.9 40 54
Total: 20 41 5.6 41 54
Percentage of the requests served within a certain time (ms)
50% 41
66% 41
75% 45
80% 45
90% 50
95% 51
98% 53
99% 53
100% 54 (longest request)
Closing Thoughts
In my endeavor to illustrate the benefits of shifting an application cache to an edge layer for enhanced speed through a simplified API caching strategy example, the most unexpected finding from the benchmark tests was Ruby's remarkable progress. The last comparison I made between openresty, Ruby, and Node.js was several years back, where Node.js distinctly outperformed Ruby. At that time, I believe Ruby was around versions 2.2 to 2.3, although I'm not certain of the exact version. For enthusiasts of Ruby facing suggestions to transition to Node.js, I recommend conducting your own benchmarks with Ruby 3.2.x. The advancements in Ruby's JIT are noteworthy. In numerous iterations of apache bench, not once did Node.js surpass Ruby.
All testing was performed by the code at ccyphers/api_cache_comparison
- cache.lua
- bench/raw_nodejs_server.js
- bench/sinatra_app.rb