Building a NaaS Platform: Node Orchestration, K8s, Billing
Running a blockchain node by hand is a simple task for a single node. When you have a hundred, it becomes an infrastructure project with K8s, StatefulSet, snapshot bootstrap, and billing. We specialize in turnkey Node-as-a-Service development and have built several such platforms: from client selection (Ethereum, Solana, BNB) to a production-grade API Gateway with rate limiting and compute units. In practice: one Fortune 500 client replaced manual node management with NaaS—infrastructure costs dropped by 40%, saving $15,000 per month, and over $180,000 annually. Contact us—we'll evaluate your project in 2 days.
How Does a Node-as-a-Service Platform Work?
A NaaS platform provides clients with a single RPC endpoint backed by orchestration of dozens or hundreds of nodes. Each node runs in K8s as a StatefulSet with its own PersistentVolumeClaim. For the budget segment, nodes are shared among clients (shared); for demanding clients, they are fully dedicated (dedicated) or deployed in clusters with load balancing (node clusters).
Why Standard Kubernetes Doesn't Fit Blockchain Nodes?
A regular Deployment in K8s doesn't account for blockchain node specifics, so Kubernetes for blockchain infrastructure requires StatefulSet. Nodes need stateful storage (hundreds of gigabytes), fixed P2P ports, and protection from restarts without losing sync. We use StatefulSet with PVC and headless service—this ensures that on failure the pod isn't recreated on a different node, and data remains tied to storage.
Example StatefulSet configuration for an Ethereum node:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ethereum-geth
spec:
serviceName: "geth"
replicas: 1
selector:
matchLabels:
app: ethereum-geth
template:
spec:
containers:
- name: geth
image: ethereum/client-go:v1.13.14
args: ["--datadir=/data", "--http", "--http.addr=0.0.0.0", "--http.vhosts=*", "--http.api=eth,net,web3,txpool", "--ws", "--ws.addr=0.0.0.0", "--maxpeers=50", "--cache=4096"]
ports:
- containerPort: 8545
- containerPort: 8546
- containerPort: 30303
protocol: TCP
- containerPort: 30303
protocol: UDP
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "16Gi"
cpu: "4"
limits:
memory: "32Gi"
cpu: "8"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-nvme"
resources:
requests:
storage: 3Ti
Problems We Solve
Snapshot bootstrapping
Synchronizing the Ethereum mainnet from scratch (snap sync) takes 12–24 hours, and an archive node takes up to 5 weeks. For NaaS, this is critical: clients pay from the first minute. We use snapshot distribution: we create an up-to-date database copy every 7 days, with incremental diffs daily. Node bootstrapping from a snapshot takes 10–15 minutes.
Comparison of Ethereum node sync modes:
| Mode | Data Size | Sync Time | RPC Availability |
|---|---|---|---|
| Snap sync | ~500 GB | 12–24 h | Full |
| Full sync | ~1.2 TB | 3–5 days | Archive |
| Archive | ~15 TB | 5–7 weeks | Archive + tracing |
Client isolation
A single platform hosts startups with a free tier and enterprises with SLA guarantees. We allocate resources via three multitenancy models, each with its own blockchain infrastructure billing approach.
| Model | Isolation | Typical Use Case | Pricing Example |
|---|---|---|---|
| Shared | Low (single process) | Free tier, test projects | Pay per CU |
| Dedicated | High (dedicated node) | Production, stable RPC | Fixed rate |
| Node cluster | Maximum (replicas + LB) | Enterprise, HA | Custom quote |
For blockchain infrastructure billing, we use compute units—each RPC method has a weight in CU: eth_blockNumber — 10 CU, eth_call — 26 CU, trace_replayTransaction — 75 CU.
How We Do It: Stack and Case Studies
RPC Proxy with Intelligent Routing
A custom Go proxy filters dangerous methods (e.g., debug_* only for premium), distributes requests between archive and full nodes, and caches responses (TTL — 1 second for eth_blockNumber). Rate limiting is implemented via Redis sliding window—it's more accurate than token bucket for RPC loads.
// Example RPC proxy with routing logic
package proxy
type RPCRouter struct {
archivePool NodePool
fullNodePool NodePool
cacheClient *redis.Client
}
var archiveMethods = map[string]bool{
"eth_getBalance": true,
"eth_call": true,
"eth_getStorageAt": true,
"trace_call": true,
"trace_replayTransaction": true,
}
func (r *RPCRouter) Route(req *RPCRequest) NodePool {
if archiveMethods[req.Method] {
if req.RequiresHistoricalBlock() {
return r.archivePool
}
}
return r.fullNodePool
}
func (r *RPCRouter) Handle(w http.ResponseWriter, req *RPCRequest, apiKey string) {
cacheKey := req.CacheKey()
if cached, err := r.cacheClient.Get(ctx, cacheKey).Bytes(); err == nil {
w.Write(cached)
return
}
pool := r.Route(req)
node := pool.GetHealthyNode()
resp := node.Forward(req)
if req.IsCacheable() {
r.cacheClient.Set(ctx, cacheKey, resp, req.CacheTTL())
}
r.billing.RecordRequest(apiKey, req.Method, resp.ComputeUnits())
w.Write(resp)
}
Health Checking with Node State Awareness
Ping doesn't guarantee the node is processing requests. We use sync progress checks: if SyncProgress is not nil or the block is older than 2 minutes, the node is excluded from the pool. Health checks run every 15 seconds.
type NodeHealthChecker struct {
client *ethclient.Client
}
func (h *NodeHealthChecker) IsHealthy(ctx context.Context) (bool, error) {
syncing, err := h.client.SyncProgress(ctx)
if err != nil {
return false, err
}
if syncing != nil {
return false, fmt.Errorf("node is syncing: %d/%d",
syncing.CurrentBlock, syncing.HighestBlock)
}
header, err := h.client.HeaderByNumber(ctx, nil)
if err != nil {
return false, err
}
blockAge := time.Since(time.Unix(int64(header.Time), 0))
if blockAge > 2*time.Minute {
return false, fmt.Errorf("block too old: %v", blockAge)
}
return true, nil
}
Rate Limiting on Redis
func (rl *RateLimiter) Allow(ctx context.Context, apiKey string, rps int) (bool, error) {
now := time.Now().UnixMilli()
window := int64(1000)
pipe := rl.redis.Pipeline()
pipe.ZRemRangeByScore(ctx, apiKey, "0", strconv.FormatInt(now-window, 10))
pipe.ZCard(ctx, apiKey)
pipe.ZAdd(ctx, apiKey, redis.Z{Score: float64(now), Member: now})
pipe.Expire(ctx, apiKey, 2*time.Second)
results, err := pipe.Exec(ctx)
count := results[1].(*redis.IntCmd).Val()
return count < int64(rps), nil
}
Billing Based on Compute Units
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
key_hash BYTEA NOT NULL,
tier VARCHAR(20) NOT NULL,
rate_limit_rps INTEGER NOT NULL,
monthly_cu_limit BIGINT,
node_type VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE usage_records (
id BIGSERIAL PRIMARY KEY,
api_key_id UUID NOT NULL REFERENCES api_keys(id),
method VARCHAR(100) NOT NULL,
chain_id INTEGER NOT NULL,
compute_units INTEGER NOT NULL,
response_time_ms INTEGER,
recorded_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_usage_billing ON usage_records (api_key_id, recorded_at);
How We Build a NaaS Platform: Phases and Timeline
- Analysis and audit (1–2 weeks): define target blockchains, multitenancy model, SLA requirements, and region.
- Architecture design (1–2 weeks): select clients (Geth, Reth, Erigon, Solana Agave), prepare K8s schemas, API Gateway, billing.
- Core infrastructure implementation (4–6 weeks): StatefulSet templates, snapshot bootstrap pipeline, health checker.
- API Gateway and billing development (6–8 weeks): RPC proxy, rate limiting, compute units, Stripe integration.
- Observability and self-service portal (6–9 weeks): Prometheus + Grafana, alerting, web interface for key management and metric viewing.
- Testing and deployment (2–3 weeks): load testing, security audit, production launch.
Total: 16 to 23 weeks to a production-ready platform. If you want to accelerate, contact our engineers—we'll suggest an appropriate pace.
What's Included
- Documentation: architecture diagrams, chain addition instructions, on-call runbook.
- Access: template repository, CI/CD pipeline, monitoring (Grafana dashboards).
- Training: 2–3 sessions for your team (DevOps and backend).
- Support: 3 months post-launch (bug fixes, consultations).
Common Mistakes in NaaS Development
- Using hostNetwork for P2P ports—loses isolation. Better use NodePort or LoadBalancer with a fixed port per node.
- Lack of caching for frequent RPC methods (
eth_chainId,eth_blockNumber)—increases node load and billing. - Health check only via TCP—the node may be alive but hundreds of blocks behind the network.
If you've encountered these issues or want to avoid them, order NaaS platform development from us. Learn more about StatefulSet. We have 10+ years of experience in blockchain infrastructure and have completed 50+ projects, including platforms for Fortune 500. Contact us—we'll evaluate your task and propose the optimal solution.







