For years, I saw companies maintain hundreds of servers to host WordPress websites for their clients. A new VPS was needed for roughly every 10 websites. Yet each page could still take around 100 ms to render. Multilingual websites often made the situation worse.
The site could still be slow when only one user was visiting it. The usual response was to add another cache layer, a CDN or another service around the application.
Each layer is reasonable by itself. Together, they create a large amount of work around what should be a simple operation:
Receive a request
Find the site
Find the route
Load the data
Render the page
Return the bytes
We wanted to make this problem go away by rethinking how a dynamic website should render a page.
The question was:
How many independently managed dynamic websites can one small server operate while still rendering real dynamic content?
The experiment eventually produced two results that surprised us:
- 5,000 independently addressable dynamic websites on one 2-vCPU, 4-GB VPS.
- A dynamic page with ten blog items rendered in under 1 ms on the prepared server-render path.
These were separate controlled benchmarks with different measurement boundaries. The reports, raw data and reproduction materials are public:
The numbers are interesting, but the more useful question is how the system was designed to make them possible.
Removing the request pipeline
The usual architecture looks something like this:
HTTP server
-> application runtime
-> framework
-> API layer
-> database
-> serializer
-> cache
-> template engine
-> response
Kooboo takes a more integrated approach:
HTTP request
-> site and route
-> prepared render plan
-> required data
-> UTF-8 output
-> network response
We built the web server, database engine and render engine as parts of the same system. The goal was to make the complete request path work together, without wasting time at every boundary between separate services.
The request does not need to cross a process boundary to query the database. The renderer does not need to serialize an intermediate result into another format before using it. The final response does not need to pass through several independent services that each rebuild part of the same context.
The goal was to remove unnecessary work from the hot path.
Our own database and render engine
Using an integrated database and render engine gives us control over the complete path from data to HTML.
The database does not need to return a generic result that is then converted several times by unrelated layers. The render engine understands the page structure, the data query and the output buffer as one operation.
This does not make every workload automatically fast. It gives us the ability to optimize the operations that happen repeatedly on every request.
Render the result as bytes
HTML eventually becomes bytes on the network.
A string is an object on the managed heap. Creating, joining and encoding many strings creates allocations that the garbage collector later has to process. We therefore keep much of the rendering path in byte arrays and write UTF-8 output directly toward the response.
The path is closer to:
data
-> UTF-8 output buffer
-> response
This reduces repeated conversion and allocation. It also makes it possible to reuse buffers and write the completed output directly toward the network.
That sounds like a small implementation detail. At high request counts, small allocations and conversions become part of the architecture.
Prepare the work before the request arrives
The renderer does not need to rediscover the entire structure of a page on every request.
A page can be represented as a prepared sequence of operations:
- write static HTML
- load a field
- execute a query
- render a repeated item
- render a component
- write the next section
The expensive work happens before the request. The request then executes the prepared plan and supplies the changing data.
We use memory-mapped files, or MMF, for prepared render plans and related render data. This gives the runtime a compact representation that can be accessed without rebuilding the complete structure for every request.
The intended transition is:
Every request:
parse
interpret
discover
allocate
to:
Before the request:
prepare
organize
optimize
During the request:
execute
write
This matters even more when thousands of sites share one process.
Make every site independently verifiable
The 5,000 sites were not copies of one endpoint.
Each site had its own:
- hostname
- route
- editable content
- site identity
- response marker
A separate server sent HTTPS requests across all hostnames. Every completed response was downloaded and checked for the expected marker.
That last part was essential. A fast response containing the wrong tenant's content would be a failure, not a success.
The capacity benchmark recorded:
5,000 independent dynamic sites
90,000 planned HTTPS requests
89,969 verified responses
99.97% first-attempt success
0 HTTP errors
0 content mismatches
Measure rendering separately from the network
The second benchmark measured the prepared warm server-render path.
Each page dynamically queried and displayed ten blog items. Page caching and full-page output caching were disabled. The benchmark completed 200,000 verified renders across the 5,000 sites.
The result:
98.3045% below 0.5 ms
99.001% below 1 ms
p50: 0.306 ms
p95: 0.443 ms
p99: 0.991 ms
Failures: 0
This is server-render latency. It is not the complete time experienced by a browser over the public Internet. Network-inclusive timing is reported separately.
That distinction matters. Rendering can complete in less than one millisecond while connection setup, TLS, network distance and client download take longer.
What these results do and do not prove
These benchmarks do not prove that every possible collection of 5,000 production websites will behave identically.
They show what this architecture achieved for a defined workload, on defined hardware, with reproducible measurements.
The next tests should use more varied workloads:
- different page structures
- larger records
- authenticated requests
- writes mixed with reads
- file and image delivery
- background jobs
- less predictable traffic
- cold and warm execution
- different tenant sizes
The interesting question is no longer only:
How fast can a page render?
It is also:
What becomes possible when a dynamic website requires very little infrastructure?
Could a small company run its own business application without assembling a large technical stack? Could an agency deliver more applications with a smaller team? Could a website become an operational system rather than a collection of pages?
I would like to hear how you would challenge this architecture.
Which workload would you test next? Which part of the benchmark would you distrust first? And where do you think the real limit will appear: CPU, memory, storage, network, isolation or operational complexity?
Top comments (0)