Community Perk$300 in Akamai Cloud credits for buildersClaim your credits
cloud-native

Bridge Edge Functions and LKE with Akamai Valkey Managed Database

When building modern distributed cloud applications today, you almost always hit a fundamental architectural tension: You want lightweight, ultra-fast logic executing right at the network edge close to your users, but you need long-running, heavy processing happening somewhere behind that—usually on container orchestrators like Kubernetes.

The real engineering challenge? How do you reliably connect these two tiers without introducing latency bottlenecks, managing fragile HTTP webhooks, or drowning in operational state overhead?

With the release of Akamai Valkey Managed Database, we finally have an open-source, high-availability, fully managed state store that acts as the mission-critical glue bridging serverless workloads on Akamai Functions with containerized worker pools running on Linode Kubernetes Engine (LKE).

In this article, we’ll look at an end-to-end distributed application from scratch: A serverless workload running on Akamai Functions pushing order events into Valkey streams, and workers on LKE consuming them at scale using consumer groups.

What You Need To Follow Along

To keep things as pragmatic as possible, we’ll implement our edge function using TypeScript on top of the CNCF Spin Framework and deploy our worker microservice onto an LKE cluster. Here is what you’ll need on your machine:

  • Node.js (version 26.7.0 or newer)
  • Spin CLI (version 4.0.2 or newer)
    • An Akamai Functions account and the Spin plugin aka
  • Docker to containerize the app that will be deployed to k8s
  • kubectl configured to talk to your LKE cluster
  • An instance of Akamai Valkey Managed Database

The Sample Repository

Open sourceReady to RunGrab the full sample code and run it yourself!akamai-developers/distributed-application-on-akamai-cloud

Why Valkey Matters in 2026

Before we dive into the codebase, let’s address the open-source context. Following the licensing shifts around Redis, the cloud-native ecosystem required a true, open-source, community-governed alternative backed by the Linux Foundation. Valkey is that exact drop-in replacement, retaining 100% wire-protocol compatibility.

Akamai’s commitment to open source means you get a fully managed Valkey service that respects open standards—meaning no vendor lock-in on protocol or data structures. You get high-availability clustering, automatic failover, and automated backups out of the box without worrying about provisioning underlying nodes.

What’s The Application All About

Our distributed application follows a clean state-decoupled blueprint:

Architecture Flow

  1. Edge Ingress: An HTTP POST endpoint running on Akamai Functions accepts order payloads at the edge and appends them directly into a Valkey Stream (orders).
  2. Kubernetes Consumer Pool: A worker deployment on LKE uses Valkey Consumer Groups to read from orders, process orders, and acknowledge completion.
  3. Atomic Aggregation: Upon processing, the worker increments global order-specific metrics and stores these as simple Key-Value pairs in Valkey.
  4. Edge Egress: An HTTP GET endpoint on Akamai Functions queries Valkey to return real-time aggregated metrics back to the caller.

1. Akamai Functions & Valkey Streams

Let’s inspect the code running at the edge. We have two TypeScript HTTP handlers compiled into WebAssembly components using the Spin SDK.

Because Valkey maintains 100% protocol compatibility with Redis, we don’t need a custom, vendor-specific driver. Standard APIs provided by the Spin SDK just work!

Submitting Orders via XADD

When a client sends a valid JSON payload to the POST /api/orders endpoint, the Spin application connects to our managed Valkey instance and appends the payload directly to a Valkey stream. Valkey streams give us an append-only, highly persistent queue out of the box without needing to run a separate, heavy message broker like Kafka.

import {
  open,
  type RedisConnection,
  type RedisParameter,
  type RedisResult,
} from '@spinframework/spin-redis';

export interface Config {
  valkeyUrl: string;
  streamName: string;
}

export function submitOrder(config: Config, order: Order): string {
  const conn = open(config.valkeyUrl);
  const result = conn.execute('XADD', [
    bin(config.streamName),
    bin('*'),
    bin('data'),
    bin(JSON.stringify(order)),
  ]);
  return resultToString(result[0]);
}

Fetching Data From Valkey Via GET

The second endpoint, GET /api/metrics, is even simpler. It queries our Valkey key-value state to fetch real-time aggregated stats written by our Kubernetes workers:

export interface Metrics {
  totalOrders: number;
  failedOrders: number;
  invalidRequests: number;
}

const COUNTER_KEYS = {
  total: 'total_orders',
  failed: 'failed_orders',
  invalid: 'invalid_requests',
} as const;

export function getMetrics(config: Config): Metrics {
  const conn = open(config.valkeyUrl);
  return {
    totalOrders: readCounter(conn, COUNTER_KEYS.total),
    failedOrders: readCounter(conn, COUNTER_KEYS.failed),
    invalidRequests: readCounter(conn, COUNTER_KEYS.invalid),
  };
}

function readCounter(conn: RedisConnection, key: string): number {
  const raw = conn.get(key);
  if (!raw) return 0;
  const value = Number.parseInt(decoder.decode(raw), 10);
  return Number.isNaN(value) ? 0 : value;
}

2. LKE Workers & Consumer Groups

To handle high-throughput processing reliably, the worker uses Valkey Consumer Groups. This ensures that even if we scale our Kubernetes Deployment to 10 replicas, each message in the orders stream is delivered and processed exactly once across these replicas on our cluster.

First, let’s explore how to create a consumer group using xgroup:

export async function ensureConsumerGroup(client: Redis): Promise<void> {
  try {
    await client.xgroup("CREATE", config.STREAM_NAME, config.CONSUMER_GROUP, "$", "MKSTREAM");
    logger.info(`Consumer group "${config.CONSUMER_GROUP}" created on stream "${config.STREAM_NAME}"`);
  } catch (err: Error) {
    if (err.message.includes("BUSYGROUP")) {
      logger.info(`Consumer group "${config.CONSUMER_GROUP}" already exists — continuing`);
    } else {
      throw err;
    }
  }
}

We want to keep our worker around until someone terminates it. (A valid reason for that might be scaling-in the deployment or rolling out a new version…). See the while loop that keeps on reading messages from the orders stream using xreadgroup:

while (running) {
    const result = (await client.xreadgroup(
      "GROUP",
      config.CONSUMER_GROUP,
      config.CONSUMER_NAME,
      "COUNT",
      "10",
      "BLOCK",
      "2000",
      "STREAMS",
      config.STREAM_NAME,
      ">"
    )) as XReadGroupResult;

    if (!result) continue; // BLOCK timeout — no messages, loop again

    for (const [, messages] of result) {
      for (const [id, fields] of messages) {
        await handleMessage(client, id, fields);
      }
    }
  }

Each message is passed into the handleMessage function. I’ve removed the code that actually validates the incoming message, but you can spot that in the sample repository - if you want to. Instead I want to highlight the message acknowledgement (xack) and atomic increments (incr) being applied conditionally. Only valid messages that were processed successfully will be acknowledged by our consumers:

async function handleMessage(client: Redis, id: string, fields: string[]): Promise<void> {
  // ...
  if (isOrder(parsed)) {
    try {
      await processOrder(parsed);
      try {
        await client.xack(config.STREAM_NAME, config.CONSUMER_GROUP, id);
        await client.incr("total_orders");
      } catch (err: Error) {
        logger.warn(`Order ${parsed.id} processed but post-ACK failed: ${err.message}`);
      }
      logger.info(`Order ${parsed.id} (Total: ${parsed.total}) processed successfully on ${HOSTNAME}`);
    } catch (err: Error) {
      logger.error(`Failed to process order on ${HOSTNAME} — id=${id}: ${err.message}`);
      // Do NOT ack — message stays in PEL for recovery
      await client.incr("failed_orders").catch(() => {});
    }
  } else {
    await client.incr("invalid_requests").catch(() => {});
    logger.warn(`Invalid or unrecognised message — id=${id}`);
    // Do NOT ack — malformed messages stay in PEL
  }
}

Deployment

The Spin application allows you to specify connection information for your Akamai Valkey Managed Database instance when deploying to your Akamai Functions account. Provide individual values for variables starting with VALKEY_ in the next snippet to deploy the order-gateway to your Akamai Functions account:

export valkey_host="foo"
export valkey_username="bob"
export valkey_password="secret"

spin aka deploy --build --variable valkey_host=$valkey_host \
  --variables valkey_username=$valkey_username \
  --variables valkey_password=$valkey_password

The spin aka deploy command gives you the public endpoint of your application. Note it down, we’ll need it in the next section.

To deploy the consumers to your LKE cluster, ensure kubectl is pointing to the correct cluster first.

We start on Kubernetes by creating a new Secret for holding the Valkey connection string. Replace valkey_url at the beginning of the snippet with your actual Valkey connection string:

export valkey_url="rediss://...."

kubectl create secret generic order-processor-secret \
  --namespace default \
  --from-literal=VALKEY_URL=$valkey_url

The Git repository contains a ready-to-use Makefile at ./src/order-processor/Makefile allowing you to:

  • Create multi-arch container images using buildx and push them to an OCI-compliant registry
  • Deploy the consumers to Kubernetes

Change the image variable at the beginning of the following script and provide a valid OCI reference that works for you:

export image=ttl.sh/order-processor:24h

# Ensure you're in the src/order-processor/ folder
pwd
# expected: /.../src/order-processor

# Build and Push container images
make push IMAGE=$image

# Deploy to Kubernetes
make deploy IMAGE=$image

Use common kubectl commands to check the state of the worker deployment in your cluster. A few seconds after the deployment command has finished, you should find three pods in the default namespace.

You can stream logs from all of them using kubectl logs -l app=order-processor -f

End-To-End Testing

Having all components of our distributed application deployed, it’s time to do an end-to-end test. For all curl commands you must provide your individual Akamai Functions endpoint. First, query the metrics endpoint to check our initial state:

curl -s https://11111111-2222-3333-4444-555555555555.fwf.app/api/metrics
# Output: {"total_orders": 0}

Next, tail the pod logs on your LKE cluster in a dedicated terminal panel:

kubectl logs -f -l app=order-processor 

Now, simulate incoming edge traffic by sending a few order events to Akamai Functions:

curl -X POST https://11111111-2222-3333-4444-555555555555.fwf.app/api/orders \
  -H "Content-Type: application/json" \
  -d '{"orderId": "ORD-1001", "customerId": "Sasha",  "total": 49.99}'

curl -X POST https://11111111-2222-3333-4444-555555555555.fwf.app/api/orders \
  -H "Content-Type: application/json" \
  -d '{"orderId": "ORD-1002", "customerId": "Bob",  "total": 29.99}'

curl -X POST https://11111111-2222-3333-4444-555555555555.fwf.app/api/orders \
  -H "Content-Type: application/json" \
  -d '{"orderId": "ORD-1003", "customerId": "Alice",  "total": 15.99}'

As soon as these HTTP calls hit Akamai Functions, the edge logic executes XADD against Valkey. Instantly, your containerized worker on LKE picks up the messages via XREADGROUP, processes the business logic, acknowledges each event with XACK, and updates the shared counter via INCR.

Executing the metrics call one more time confirms the state sync:

curl -s https://11111111-2222-3333-4444-555555555555.fwf.app/api/metrics
# Output: {"total_orders": 3}

Low-latency state propagation from the Edge to Kubernetes without managing a sophisticated message broker infrastructure! 🤘🏼

Recap

Decoupling edge execution from heavy backend processing no longer requires complex networking hacks or bespoke state management. By leveraging Akamai Valkey Managed Database as your distributed state glue, you get open-source peace of mind, high availability, and blazing-fast performance across Akamai Functions and LKE.

If you want to inspect the complete codebase, grab the manifests, or deploy this architecture yourself, check out the full project on GitHub. If you have questions or want to discuss edge-native state patterns, come hang out with us in Edge Case, our developer Discord community.

See you there!

Thorsten Hans
Thorsten Hans
Sr. Developer Advocate

Thorsten Hans is a Senior Developer Advocate at Akamai, Docker Captain, and Wasm enthusiast. He thrives on pushing the limits of WebAssembly and Edge Computing to help developers build high-performance distributed systems. Thorsten shares his technical deep-dives via his blog and on global stages to shape the cloud's future.