Repository navigation
Warmed up cache slows the query #216
Description
The problem
Initial query on a cold cache executes faster than with a warmed one.
Reproduction
Here is the test I've been using
class TestDomain
ROOT_NAMESERVERS = %w[
a.root-servers.net
b.root-servers.net
c.root-servers.net
d.root-servers.net
e.root-servers.net
f.root-servers.net
g.root-servers.net
h.root-servers.net
i.root-servers.net
j.root-servers.net
k.root-servers.net
l.root-servers.net
m.root-servers.net
].freeze
def name
"example.com"
end
def recursive_query(type, ns: ROOT_NAMESERVERS)
return [] unless ns.present?
msg = nil
puts "Time: #{Benchmark.realtime { msg = resolve_query(type, ns: ns, recurse: false, cache: ROOT_NAMESERVERS.intersect?(ns)) }}"
return [] unless msg
# The answer is received.
return msg.answer.map { |rr| rr.rdata_to_string } if msg.answer.present?
# Another layer of intermediaries.
authority = msg.authority.
find_all { |rr| rr.type == Dnsruby::Types.NS }.
map(&:rdata).map(&:to_s).reject(&:blank?)
return [] unless authority.present?
recursive_query(type, ns: authority)
end
private
def resolve_query(type, ns: nil, recurse: true, cache: true)
puts "Caching: #{cache} | NS: #{ns.first}"
resolver = Dnsruby::Resolver.
new(recurse: recurse, do_caching: cache, retry_times: 2)
resolver.nameserver = [ns].flatten.shift
resolver.query(name, type)
end
endI tested this from a Rails console environment, and the numbers look strange:
Loading development environment (Rails 8.0.2)
3.4.2 :001 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.12582800001837313
Caching: false | NS: l.gtld-servers.net
Time: 0.12042500008828938
Caching: false | NS: a.iana-servers.net
Time: 0.12831000005826354
=> 0.3755820000078529
3.4.2 :002 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.21918399981223047
Caching: false | NS: l.gtld-servers.net
Time: 0.32541300007142127
Caching: false | NS: a.iana-servers.net
Time: 0.32564599998295307
=> 0.8710799999535084
3.4.2 :003 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.22562899999320507
Caching: false | NS: l.gtld-servers.net
Time: 0.3200809999834746
Caching: false | NS: a.iana-servers.net
Time: 0.3220410000067204
=> 0.8686989999841899And even if you switch the caching to be always true, it is still kind of slow:
Loading development environment (Rails 8.0.2)
3.4.2 :001 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.1284089998807758
Caching: true | NS: l.gtld-servers.net
Time: 0.12005099980160594
Caching: true | NS: a.iana-servers.net
Time: 0.12974500004202127
=> 0.37913599982857704
3.4.2 :002 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.21118599991314113
=> 0.21136899990960956
3.4.2 :003 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.21160500007681549
=> 0.2118849998805672I saw this once or twice, which seemed like the cache was hit, but in most cases, it doesn't seem to work.
3.4.2 :002 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
Time: 0.001236999873071909
=> 0.0012920000590384007When the cache is cold, the query executed in 0.12582800001837313, with pre-warmed cache, the same query with do_cache: true took 0.21918399981223047 instead of 0.0012920000590384007 (which I saw when the cache actually hit). This is about 170 times slower than expected with a warmed cache.
The next queries also increased from 0.12042500008828938 to 0.32541300007142127, so also around 0.2 seconds slower. It feels that the existing cache adds around 0.2 seconds to each query, even those with do_cache: true.
Activity
heaven commentedon Nov 20, 2025
I have updated the test script to benchmark each stage separately, and it seems nameserver resolution is what takes these extra 0.2 seconds.
Here is what I changed
And the test results look like this for the cold cache
And here's the warmed cache:
Looks like
do_cache: falsealso disables the NS resolution cache. And there is some inconsistency, because the first time the cache was used even for subsequent calls, the second time it didn't hit.alexdalitz commentedon Nov 20, 2025
Hi @heaven - thanks for making this new issue!
I'm afraid I'm busy until at least Monday, so won't be able to look at this until then.
Thanks!
alexdalitz commentedon Nov 28, 2025
Hi @heaven - sorry for the delay getting back to this.
I have the following output for do_caching = true :
heaven commentedon Nov 28, 2025
@alexdalitz, no worries at all. By coincidence, I've got completely overwhelmed with another project at the moment, but I will get back to this issue, hopefully next week.
alexdalitz commentedon Nov 28, 2025
Here are some results from using a normal Resolver, created anew for each query (but having already queried example.com first, to get the nameservers resolved) :
heaven commentedon Dec 4, 2025
Hi, this is the result I am seeing consistently – the second time I run the same command, it is 2 times slower than the first one. Once I restart the rails console, the first call is fast again, all the consecutive calls are slower. This is reproducible and there is definitely a problem somewhere.
And this is 100% related to the internal cache, because if I replace this line (only cache the response of the roots)
With this (never cache)
The results are consistens and the slowdown doesn't happen.
You can close the issue, if you want, but there is a 100% bug with the caching implementation, which, when enabled for the first query, and disabled for the consecutive ones, makes resolver twice slower.
This is the place where it becomes slow
First run
Consecutive runs
The degradation is severe, 10-26 times.
And here is the worst part, if I change this line to always cache
It still doesn't work consisnently
The first call took
0.5767140001989901, which looks normal, aligns with the previous results. the second one looks great! Like it should when everything is cached0.0030559999868273735. But then all the consecutive results become slower0.21197299985215068.alexdalitz commentedon Dec 5, 2025
@heaven Like I said above, I think your test code is faulty!
The results I quoted above were from making a new Resolver (and running a query to make sure the nameservers were all resolved), and then querying for a domain name. I think they clearly show that the cache is working!
alexdalitz commentedon Dec 5, 2025
I've modified your test code to make it run a proper test. It's a complete mess as a result (it would probably have been better to have made a new one) but here it is :
This is the result :
heaven commentedon Dec 11, 2025
Except that it doesn't do what we need 😀
The cache doesn't work in our case and the test I provided is exactly what we are doing – querying the roots (that list of 13 global servers), there we look for the TLDs, then we go through any other intermediaries, down until we reach the authoritative domain nameservers. I don't see where it is faulthy, to my understanding it is absolutely legit.
But, even your test fails if you run
TestDomain.new.run_three_tests("A")multiple times. Check out the timings:(this is 1.73.1)
And it looks like master got better
(this is master)
alexdalitz commentedon Dec 12, 2025
Hmmmm......
I can see there is a problem here - thanks for your persistence!
I'm a little busy right now, but I'll try to look at this next week.
Thanks!
alexdalitz commentedon Dec 15, 2025
Hi @heaven - thanks so much for sticking with this one!
The problem (which I have now fixed in master) was that, when the select thread was stopped (after a second or so of inactivity), all the open sockets were deleted - including the pipe-lining socket which was used to wake the select thread up (for example, if a cache entry was found). This meant that the wakeup signal was not received, so the select() call had to time out (currently about 0.1 seconds) before the response was handled.
I now get results like :
3.3.5 :037 > TestDomain.new.run_three_tests("dalitz.co.uk", "A")
3.3.5 :038 >
Cache true
Query Time: 0.004098000004887581
Query Time: 0.00019700010307133198
Query Time: 0.00017399992793798447
=> 3
3.3.5 :039 > TestDomain.new.run_three_tests("dalitz.co.uk", "A")
3.3.5 :040 >
Cache true
Query Time: 0.00042699999175965786
Query Time: 0.00034399982541799545
Query Time: 0.0003450000658631325
=> 3
3.3.5 :041 > TestDomain.new.run_three_tests("dalitz.co.uk", "A")
Cache true
Query Time: 0.00025800010189414024
Query Time: 0.00022600009106099606
Query Time: 0.00030399998649954796
which I think shows that the problem is fixed.
Could you please check out master and see if it works for you?
Once you're happy with it, I will make a new release.
Thanks again!
alexdalitz commentedon Dec 15, 2025
@heaven Here are the results from your original test :
3.3.5 :199 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
=> 0.026787000009790063
3.3.5 :200 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
=> 0.019549000076949596
3.3.5 :201 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
=> 0.02007699990645051
3.3.5 :202 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
=> 0.019807999953627586
3.3.5 :203 > Benchmark.realtime { TestDomain.new.recursive_query("NS") }
Caching: true | NS: a.root-servers.net
=> 0.01752300001680851
heaven commentedon Dec 15, 2025
@alexdalitz Hi, and hanks for your great efforts. I will retest from my end, hopefully by tomorrow or wednesday, but the numbers look great, now they look like the cache is actually doing its job.
alexdalitz commentedon Jan 12, 2026
Hi @heaven - have you had a chance to test this, please? It's worth noting that I've made a small change to master, so it would be worth pulling again before testing. Thanks!