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.

RouteCompute nodeProfiled estimateHeld-out p50Error
/feedCompute A2.261 ms2.338 ms0.077 ms
/feedCompute B2.451 ms2.571 ms0.120 ms
/feed-naiveCompute A5.650 ms5.978 ms0.328 ms
/feed-naiveCompute B4.449 ms4.744 ms0.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.

RouteUser-first p50Network-only p50Profiled p50Selected host
Three-query /feed2.834 ms2.816 ms2.829 msCompute A
Twelve-call /feed-naive6.356 ms5.742 ms5.690 msCompute 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 callsCompute A p50Compute B p50
01.044 ms1.499 ms
11.668 ms1.844 ms
32.603 ms2.443 ms
63.956 ms3.225 ms
126.668 ms4.555 ms
2412.039 ms7.405 ms
4822.435 ms12.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.

CallsUser-first p50Profiled p50
11.978 ms1.966 ms
32.949 ms2.865 ms
126.776 ms4.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.