One request, from the client to the actor and back
A request to a suspended actor passes the proxy, atenet-router, ate-api-server, and Postgres before a worker restores the actor, and a request to a running actor still asks all four.
You sent curl -X POST http://localhost:8000/ and a count came back. Six programs handled that request, and one of them wrote to a database before the actor ran. When a request fails later, the error text names one of those programs, so you need to know the order.
When you finish this section, you can name each hop of one request and say which hops a running actor skips. You can also say which parts of the cluster must be up for a request to pass.
The recording
We created my-counter-1 in the atespace ate-demo-counter and started kubectl ate proxy. Then we sent five requests, one after the other. This is the start of the recording. The columns are the time, the event, the HTTP status, the seconds at the client, and the body.
ts event detail
2026-11-12T10:14:21.286Z state ACTOR_STATE_SUSPENDED -
2026-11-12T10:14:21.837Z call-1 200 0.358379 preserved memory count: 1 | preserved file counter: 1 | preserved rootfs counter: 1
2026-11-12T10:14:21.944Z call-2 200 0.090770 preserved memory count: 2 | preserved file counter: 2 | preserved rootfs counter: 2
2026-11-12T10:14:22.030Z call-3 200 0.066903 preserved memory count: 3 | preserved file counter: 3 | preserved rootfs counter: 3
2026-11-12T10:14:22.109Z call-4 200 0.061842 preserved memory count: 4 | preserved file counter: 4 | preserved rootfs counter: 4
2026-11-12T10:14:22.196Z call-5 200 0.068053 preserved memory count: 5 | preserved file counter: 5 | preserved rootfs counter: 5
2026-11-12T10:14:22.203Z state ACTOR_STATE_RUNNING -We measured these requests on an x86 server guest with 24 vCPU and 47 GB, through kubectl ate proxy, in one run. The first request took 0.358 s. The next four took 0.091 s, 0.067 s, 0.062 s, and 0.068 s. Raw runs
The actor was ACTOR_STATE_SUSPENDED before the first request and ACTOR_STATE_RUNNING after the fifth. No command resumed it. The first request did.
Hop by hop
Figure 1.2 draws the first request. The numbers in this list match the rows of the figure.
curlsends the request to127.0.0.1:8000, wherekubectl ate proxylistens.- The proxy opens a port-forward through the Kubernetes API server to
atenet-router. Inside that port-forward it opens an mTLS CONNECT tunnel to router port 8444, with a client certificate. - The router reads the target actor and calls
ResumeActoronate-api-server. It does so for every request, because it keeps no record of where an actor runs. Requests that arrive at the same time for one actor share one call. ate-api-serverreads the actor row from Postgres. The state isACTOR_STATE_SUSPENDED.ate-api-servertakes a lease on the actor, chooses a worker with room, and writes the assignment to Postgres in one transaction.ate-api-servertellsateleton that node to restore the snapshot. The worker starts the sandbox from it, and the actor state becomesACTOR_STATE_RUNNING.ate-api-serverreturns the actor to the router, with the IP address of the worker pod and the identity of the worker.- The router opens a tunnel to port 8443 of the worker pod and checks that identity. The worker forwards the request to port 80 of the actor.
- The reply goes back the same way.
A new actor has never run, so the snapshot in step 6 is not its own. It is the golden snapshot of its template, and section 3.1 explains it. After step 7 the actor object carries the assignment:
"state": "ACTOR_STATE_RUNNING",
"workerAssignment": {
"nodeName": "kind-control-plane",
…
"workerEpoch": "1",
"workerNamespace": "ate-demo-counter",
"workerPod": "counter-7cc7dcd596-ffxwt",
"workerPodIps": [
"10.244.0.23"
],
…
"workerPool": "counter",
"workerSpiffeId": "spiffe://cluster.local/ns/ate-demo-counter/sa/ateworker"Where the 0.36 s goes
The first request paid for the restore. The four after it did not. The difference is about 0.29 s on this guest, for an actor that holds almost no memory.
The worker writes one log line when a restore starts and one when it ends. We did not save that log for the first request. We saved it for a later resume of the same actor:
{"time":"2026-11-12T10:30:17.846333794Z","message":"Actor restoring",…
{"time":"2026-11-12T10:30:18.098955499Z","message":"Actor restored",…We measured that resume on the same guest, once. The two log lines are 0.253 s apart, and the request that caused the restore took 0.382 s at the client. So about two thirds of a first request is the restore itself. Raw runs
That share grows with the actor. A restore reads the memory that the actor held, and section 3.3 shows 2.16 s for an actor with 1000 MiB.
A request to a running actor
A request to a running actor still does steps 1 to 4. ate-api-server reads the row, finds ACTOR_STATE_RUNNING, and returns at once. Steps 5 and 6 do not happen. So a request costs one call to ate-api-server and one read from Postgres, also when nothing is resumed. Requests that arrive together for one actor share that call.
Most of the 0.06 s in our recording is the proxy, not Substrate.
We measured both paths on the second x86 guest, with 16 vCPU and 23 GB, in the 30 s before a test. A pod that sent its requests to the router answered in 0.027 s at the median, with 30 requests. The same actor through kubectl ate proxy answered in 0.067 s at the median, with 28 requests. Raw runs
Use the number from the pod when you plan latency. The proxy is a tool for your own machine, and section 2.3 shows why it must not carry real traffic.
What must be up
Three things follow from the path.
- Postgres and
ate-api-serverare on every request path. A running actor that needs no resume still fails when either one is down. On our second guest, requests to a running actor failed within seconds of each outage. Section 8.2 has the timeline. - The Kubernetes API server is on no request path. Only row 2 of the figure passes it, and that row belongs to the proxy. In the same test, kube-apiserver was down for 86 s. In that time 103 of 103 requests from a pod were served: 86 to a running actor and 17 resumes.
- The object store is on the path only when a snapshot is not on the node. Section 3.4 says when that is.
The install runs one atenet-router pod and one Postgres pod. Every request to every actor passes both. Section 8.1 says what to do about Postgres.
The actor stays up
After the fifth request we sent nothing for 900 s. Figure 1.2b shows all six requests of the recording, and every state sample between the last two.
The sixth request took 0.080 s and returned count 6. A resume would have cost about 0.36 s. The actor was never suspended, and it held its worker for the whole 15 minutes. Section 1.1 names this as the first job that Substrate leaves to you.
Agent Substrate 1.0.0 · b34fbee · 2026-11-05 · Edition 2026.11