Placing Compute Along the Database Request Path
Where should a web request run when the host closest to the client is not the host closest to its data? Conventional routing can minimize client-to-compute delay. A database-backed request can also pay for each serial network wait between compute and the database. This paper tests whether a request-aware placement rule can reduce total response time.
The test runs one Rails service on two application compute nodes and uses a separate PostgreSQL node. It supports a narrow claim: for this service, requests with more serial database calls benefited from database-aware placement over user-first routing. The experiment does not test a general scheduler or other dependency types.
For a candidate compute host c, the measured score is:
estimated_latency(c) = client_to_compute_path(c) + N_serial × PostgreSQL_RTT(c) + route_residual(c)
N_serial counts database calls that depend on earlier calls. Parallel calls do not each add a full RTT to
the critical path.
The network-only policy omits route_residual. The profiled policy estimates it from the route's measured
zero-query cost. This is a practical model of the measured path. It is not a general model of CPU cost, queues,
or every service dependency. The experiment found no measurable placement benefit from profiling over the
simpler network-only policy.
I calibrated a route-aware cost model on Compute A and Compute B. I then changed the client-to-compute network path and tested the model on new requests. It predicted four direct-node medians within 0.08 to 0.33 ms. The three-query route was faster on Compute A. The 12-query N+1 route was faster on Compute B.
The routing proxy reduced the N+1 route median from 6.356 ms with user-first routing to 5.690 ms with the profiled policy. This is a 10.5% reduction across 500 requests per policy. The 95% bootstrap interval for the median change was 0.522 to 0.780 ms lower. The profiled policy and simpler network-only policy chose the same hosts. Their median difference was not measurable in this run.
The data guide defines the public role labels and measurement files.
Prototype and cost model
The prototype has a Python HTTP proxy and a placement planner. The same Rails service ran on both compute nodes. PostgreSQL ran on a separate database node. The app and database did not share a node. Both policies used the same strong-read primary. The proxy tested compute selection, not replica placement.
The network-only policy estimates response time as:
user_to_compute_RTT + sequential_query_count × SQL_round_trip_time
The profiled policy adds a route-specific residual for each compute host:
profiled_cost = user_request_path + host_route_residual + sequential_query_count × SQL_round_trip_time
I measured each host's /health request path and each Rails route. I measured the median time for SELECT 1
from each app host. The route residual is the direct route median minus the health request median and the
sequential query count multiplied by the SELECT 1 median. This residual captures route work that the
network-only score does not model. It does not isolate CPU time.
The /feed route makes three sequential SQL calls. The /feed-naive route makes one account call, one article
call, and ten feedback calls. The seeded account has ten articles, so the N+1 route makes twelve calls.
Calibration and held-out test
I calibrated the model without added user-path delay. For each host and route, I warmed the endpoint 20 times
and measured 500 requests. I measured 500 SELECT 1 calls after 20 warmups. The median SQL call times were
0.355 ms from Compute A to the database node primary and 0.231 ms from Compute B to the primary.
The test added 0.25 ms of one-way egress delay on the path from Compute B to Compute A. The new
client-to-compute TCP connect medians were 0.088 ms for Compute A and 0.601 ms for Compute B. The /health
request medians were 0.741 ms and 1.480 ms.
I then measured 500 new requests for each route on each host. The calibration data
and held-out direct measurements include all samples.
| Route | Compute node | Profiled estimate | Held-out p50 | Error |
|---|---|---|---|---|
/feed | Compute A | 2.261 ms | 2.338 ms | 0.077 ms |
/feed | Compute B | 2.451 ms | 2.571 ms | 0.120 ms |
/feed-naive | Compute A | 5.650 ms | 5.978 ms | 0.328 ms |
/feed-naive | Compute B | 4.449 ms | 4.744 ms | 0.295 ms |
The profile chose the compute node with the lower held-out p50 for each route. These estimates use a single calibration run. They do not show that the profile will remain accurate under load or across other machines.
Routed results
The proxy opened a new upstream connection for each request. It warmed each policy and route 15 times. It then sent 500 requests for each of three policies and two routes. The run interleaved and randomized requests. All 3,000 requests returned HTTP 200.
| Route | User-first p50 | Network-only p50 | Profiled p50 | Selected host |
|---|---|---|---|---|
Three-query /feed | 2.834 ms | 2.816 ms | 2.829 ms | Compute A |
Twelve-call /feed-naive | 6.356 ms | 5.742 ms | 5.690 ms | Compute B |
Both database-aware policies kept /feed on Compute A. Neither changed its median by a measurable amount. For
/feed-naive, network-only routing lowered p50 by 0.614 ms relative to user-first routing. Profiled routing
lowered p50 by 0.665 ms. The 95% bootstrap interval for the profiled change was 0.522 to 0.780 ms lower.
The profile did not improve the placement choice over the network-only policy. Both policies selected the same
host for each route. Their /feed-naive p50 difference was 0.052 ms, with a 95% bootstrap interval from 0.191
ms lower to 0.137 ms higher. This test shows that route and host profiles can predict end-to-end cost. It does
not show that the added profile logic improves routing when the simpler policy already chooses well.
The first proxy run used only the network score. It predicted a larger gain than the measured result. The initial run data records that result. The later profile reduced prediction error, but did not produce a measurable gain over network-only routing.
A deliberate policy disagreement
The first proxy experiment did not make the network-only and profiled policies choose different hosts. I added
a controlled Rails route that runs N serial SELECT 1 calls. Compute A and Compute B are the two
application nodes. PostgreSQL stayed on the separate database node. The same client sent requests to both
application nodes.
I swept N = 0, 1, 3, 6, 12, 24, and 48. The network model used the measured /health path plus N times the
per-query database time. The profiled model used the measured zero-query route plus N times the per-query
database time. On the first calibration, the network model's predicted crossover was after 3.55 calls. The
profiled model predicted it after 1.62 calls. The
primary calibration records the inputs.
| Serial calls | Compute A p50 | Compute B p50 |
|---|---|---|
| 0 | 1.044 ms | 1.499 ms |
| 1 | 1.668 ms | 1.844 ms |
| 3 | 2.603 ms | 2.443 ms |
| 6 | 3.956 ms | 3.225 ms |
| 12 | 6.668 ms | 4.555 ms |
| 24 | 12.039 ms | 7.405 ms |
| 48 | 22.435 ms | 12.743 ms |
The network model chose Compute A at zero, one, and three calls, then chose Compute B from six calls on. The profiled model switched to Compute B at three calls.
At three calls, Compute B was 0.161 ms faster than Compute A. The 95% bootstrap interval for Compute A p50 minus Compute B p50 was 0.046 to 0.260 ms. At one call, Compute A was faster by 0.176 ms, with an interval from -0.246 to -0.115 ms. The raw sweep and summary include all samples. A 500-sample follow-up at two calls found no clear winner. Compute A was faster by 0.006 ms, with a 95% interval from -0.075 to 0.048 ms. The two-call follow-up is available. The data place the measured crossover between one and three calls, but do not locate it more precisely.
I repeated the direct comparison with a fresh calibration. The new network and profiled models both chose Compute A at one call and Compute B at three and twelve calls. Compute B was faster at three calls by 0.243 ms, with a 95% interval from 0.178 to 0.314 ms. This repeat supports a workload-dependent placement change. It does not show that the profiled model improves on the simpler network model. The measured host paths varied enough to move the predicted crossover. The repeat calibration, measurements, and summary are available.
I then sent requests through all three local proxies, using the fresh calibration. The same client sent one-call, three-call, and twelve-call requests. User-first routing kept all requests on Compute A. The two database- aware policies sent the one-call request to Compute A and the longer requests to Compute B.
| Calls | User-first p50 | Profiled p50 |
|---|---|---|
| 1 | 1.978 ms | 1.966 ms |
| 3 | 2.949 ms | 2.865 ms |
| 12 | 6.776 ms | 4.918 ms |
Profiled routing reduced the three-call p50 by 0.083 ms relative to user-first routing. Its 95% interval was 0.010 to 0.187 ms. At twelve calls, it reduced p50 by 1.858 ms, with an interval from 1.616 to 2.050 ms. Profiled routing did not measurably beat network-only routing. Their median difference was 0.022 ms at three calls, with an interval from -0.056 to 0.100 ms, and 0.059 ms at twelve calls, with an interval from -0.230 to 0.289 ms. The proxy samples and summary contain these measurements. Network-only p50 was 1.973 ms, 2.887 ms, and 4.977 ms at one, three, and twelve calls. The two database-aware policies chose the same host at each count in this repeated run.
This test supports route-dependent database-aware placement over user-first placement for this Rails service. It does not establish a benefit from profiling over a simpler policy that combines measured client-path and database costs. The sweep, proxy, measurement script, and Rails route are published with the data.
Isolation and limits
The application and database ran on separate nodes. The experiment did not test hostile tenants, a hardened sandbox, or failure isolation. It used one database primary, so it did not test compute and replica selection together.
This test used one Rails service, two application nodes, one database node, one client path, and one controlled delay. It did not model concurrent arrivals, queueing, failures, migration, or production traffic. The measured route split supports selective compute placement for this workload. It does not establish a production scheduler benefit.
The general scheduling question extends beyond PostgreSQL. A request can depend on several services, and each serial wait can affect the best compute location. This experiment did not measure Redis, object storage, or service-to-service calls. Future work can test a placement score that includes those dependencies and host compute cost.
Latency-aware placement and compute-data colocation have substantial prior work. NoMora uses measured application performance and network delay to place or move tasks. LambdaStore colocates serverless functions with storage. User-aware edge placement includes user latency in service placement. Corral jointly places compute and data while isolating jobs. This prototype does not claim that these broad ideas are new.