← All posts

From a 20-second dashboard to 300 ms

OriginChainDB · May 13, 2026
infrastructure performance platform operations

The dashboard’s cold-path latency was a credential fetch, not the database. Reading each tenant’s bearer token with a remote command to the instance cost 4-15 seconds; moving that read to a managed parameter store cut it to ~150 ms, and the cold page load from 20 seconds to ~300 ms.

A customer mentioned in a thread that the schema-list page on their OriginChainDB dashboard took “like around 20 seconds.” Not a feature request: a complaint. The page renders four schemas. Each is a hundred-byte manifest. The engine serving them, behind a TLS connection, responds to GET /v1/tenants/.../schemas in ~350 ms end-to-end including the TLS handshake.

So where do the other ~19 seconds go? They went into how the dashboard backend authenticated to the tenant engine on the user’s behalf.

The architecture and the problem

OriginChainDB’s customer dashboard lives at app.originchain.ai. The backend serving that dashboard does cookie-auth against the user. When the user opens a schema page, the backend translates the request into a tenant-bearer call against the customer’s dedicated engine instance. The tenant bearer is the customer’s secret; it is written onto the instance at provision time.

The backend needs that bearer to make the proxy call. It doesn’t have it in memory. It has to ask the tenant box for it. The original implementation issued a remote command to the instance to read the credential file off disk and return its contents, then polled for the result:

1. issue a remote "read this credential" command to the instance
2. wait for the command to be dispatched
3. the instance runs it and returns the credential on stdout
4. poll for completion every 500 ms
5. once complete, read the result back

The remote-command API we used is beautifully versatile and operationally awful for a request that’s on the hot path of a page load. The wire flow involves spawning a helper process (~1 s cold start), a multi-second control round-trip to dispatch the command, a hop to the agent on the target instance, and then repeated polling - each poll paying the same cold-start cost again.

Cold path: 4–15 s typical, with a tail at 20 s. The result was cached in-process for 5 minutes, so most page loads were fast. But every cache miss - first load of the day, first load after a backend restart, first load after the 5-min idle - paid the full price.

When the user hits a dashboard page on a Monday morning, that’s the cache miss. Hence “20 seconds.”

Why the original design picked a remote command

Hindsight makes this look silly, but at the time it was reasonable:

These are individually fine reasons. The sum of them, weighted by the fact that the call happens on every dashboard request after a 5-minute gap, was a bad trade.

The fix

Three things:

1. Write the bearer to a managed parameter store at provision time

Per tenant, encrypted at rest, under a per-instance path. The provision flow already mints the bearer and writes it to the instance; adding a parameter-store put alongside is a few lines in the instance-create handler:

let bearer = generate_bearer;
let bearer_hash = password::hash(&bearer)?;

//... existing DB insert + provisioning trigger...

bearer_store::put(instance_id, &bearer).await?;

The decrypt permission was already granted to the backend’s role; the put/get permission on the per-tenant path is a fresh statement.

2. Replace the bearer fetch with a direct call to the parameter store

pub async fn get(instance_id: Uuid) -> AppResult<Option<String>> {
 let client = param_client.await?;
 let name = format!("/oc/tenant-bearers/{}", instance_id);
 match client.get_parameter
.name(&name)
.with_decryption(true)
.send.await
 {
 Ok(out) => Ok(out.parameter.and_then(|p| p.value).map(String::from)),
 Err(err) if matches!(err.as_service_error,
 Some(GetParameterError::ParameterNotFound(_))) => Ok(None),
 Err(e) => Err(AppError::Internal(format!("param store: {e}"))),
 }
}

The cold call: one signed HTTPS GET, decrypt server-side, response in ~150 ms. The client is a process-wide singleton so the connection pool warms once and every subsequent call reuses connections.

3. Backwards-compatible fallback for unmigrated instances

The catch: existing tenants didn’t have their bearer in the parameter store yet. We could have run a one-shot migration script, but the correctness story is cleaner if the backend handles it automatically:

// Try the parameter store first. ~150 ms cold.
if let Some(bearer) = bearer_store::get(instance_id).await? {
 return Ok(cache_and_return(bearer));
}

// Fall through to the legacy remote-command read of the credential.
let bearer = fetch_tenant_bearer_via_remote_command(ctx).await?;

// Opportunistically backfill the parameter store. Next call lands fast.
if let Err(e) = bearer_store::put(instance_id, &bearer).await {
 warn!(error = %e, "bearer parameter-store backfill failed");
}
Ok(cache_and_return(bearer))

Three properties:

We also pre-seeded the live demo tenant’s bearer directly so the first dashboard load after the deploy was fast, not 15 seconds. Self-bootstrap, when you can.

Bumping the in-process cache TTL

The legacy slow path was so painful that we’d capped the in-process cache at 5 minutes - the cost of a cache miss was so high that we wanted to refresh aggressively in case the bearer had rotated externally.

With the new path at ~150 ms, the cost of a miss is small. We bumped the cache TTL to 1 hour. The bearer rotates only on an explicit rotate-bearer call (which invalidates the cache locally) or rare operator intervention; 1 hour catches an out-of-band rotation soon enough without paying needless parameter-store calls.

Live numbers, end-to-end

After deploying:

PathBeforeAfter
Cache miss (1st load / 1h idle)4–15 s~300 ms
Cache hit (tab switch)<1 ms<1 ms
Tail (p99 of cache miss)20+ s~500 ms

The 300 ms vs 150 ms gap is the rest of the dashboard request: DB lookup of the user’s org + instance ownership check, the proxy call to the tenant engine, the JSON serialise. The parameter-store call itself is the smallest part of the new latency budget.

p99 dropped from “20 seconds” to “500 ms” - a 40× improvement on the worst case, ~100× on the mean. The hot path was already fast; the fix was about the cold path.

What this is NOT

Why this is worth blogging

Most database vendor blog posts are about the hot path: the smart index, the magic protocol, the new compression scheme. Cold paths shape product perception just as much. A 20-second first load makes your dashboard feel broken; a 300 ms first load makes it feel snappy even if the rest of the system is the same.

If you’re building a SaaS control plane and reaching for a remote-shell call because “it’s already there,” consider the alternative. A managed parameter store is the right primitive for any sub-kilobyte per-tenant secret, and the migration is a fallback plus a backfill.

Try it

Open the schema page on your tenant. It’ll feel faster than it used to.


← All posts Subscribe to RSS →