Monthly Archives: April 2026

Deploying & Using KEDA

We’re often using HPA (Horizontal Pod Autoscaling) or may looked at VPA (Vertical Pod Autoscaling) but there’s also KEDA (Kubernetes Event-driven Autoscaling) which is an operator to scale workloads based upon (as the name suggests) events, for example queue triggers could come from a message bus/queue such as RabbitMQ. KEDA can also be used in such scenarios to scale to 0 pods (almost like the way we might use Azure functions).

Installing

To install KEDA, just add the repo to helm then update

Note: see official documentation https://keda.sh/docs/2.19/deploy, which I’ve partially reproduced here

  • helm repo add kedacore https://kedacore.github.io/charts
  • helm repo update

Now to install KEDA run

  • helm install keda kedacore/keda –namespace keda –create-namespace

As you’ll probably expect from this line, a new namespace keda is added to our Kubernetes cluster.

Kubernetes kind ScaledObject

We’ll want to configure K8s to scale our objects. Let’s use an example already on the web as it’s a perfectly simple illustration of using RabbitMQ to trigger scaling of our service. In this case we’ll scale down to 0 so essentially when our service is not being used (obviously you’d probably only want this for less frequently uses service due to cold-start etc.) we can scale the resources to zero to free up available namespace based memory etc.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: my-scaledobject
spec:
  scaleTargetRef:
    name: my-deployment
  minReplicaCount: 0
  maxReplicaCount: 10
  triggers:
    - type: rabbitmq
      metadata:
        queueName: my-queue
        host: amqp://user:pass@rabbitmq:5672/
        mode: QueueLength
        value: "10"   # messages per pod
      authenticationRef:
        name: rabbitmq-secret 

In this example we are using ScaledObject as the kind (this is a KEDA object). This YAML will allow K8s to auto-scale my-deployment pods based upon RabbitMQ queue activity, as stated previously this will scaled down to zero and we ensure a max scaling of 10 – we need to ensure that even if the backlog on the queue is large, we’re not going to scale out of control.

The scaleTargetRef tells KEDA which K8s workload to scale, in this case we expect a deployment names my-deployment.

The triggers section tells KEDA what metric to monitor, in this case KEDA will monitor queue metrics, of the queue my-queue. When messages appear in the queue, KEDA scale up the consumer pods and when the queue drains KEDA scales down the pods.

In our example we also use authenticationRef to securely store RabbitMQ credentials within K8s secrets.

For example, if your queue suddenly receives 100 messages and with this threshold of 1- messages per pod then KEDA will scale to 10 pods.

What KEDA gives us is the ability to use event-driven scaling based upon real workloads as opposed to using CPU or memory.

More uses

KEDA has a built in Azure Blob Storage scaler, allowing is to scale based upon the number of blobs in a container, this can be useful when long running jobs are triggered by files being written into blob storage. For example, processing uploads files, batch process triggered by blobs presence etc.

Support exists for Rabbit MQ as already seen but also for Azure Service Bus where (again) we can scaled based upon queue length as well as topic and subscription message count. Examples would include REST API to worker pods scaling, background processing as well as event driven microservices.

Redis List or Redis stream scalers are supported as well as Azure Managed Redis. Here we could scale based upon job queues within Redis lists, stream based event processing etc.

References

KEDA scalers
KEDA with Azure services

Let’s talk Server-Sent Events

Server-Sent Events (SSE) are a lightweight streaming/push technology for pushing updates to a web client over a single long-live HTTP connection.

For example as use case where your web client connects to a server and periodically receives events or notifications from the server. Alternates to SSE might be long polling the server for updates, web sockets, Signal R etc. SSE is simpler than setting up web sockets and for C#/.NET developers can be seen to be similar to asynchronous streams.

All modern browser should support SSE using EventSource. SSE also supports automatic reconnection but one key thing to remember is this is only for one-way updates (unlike Signal R).

ASP.NET Sample

Let’s create an ASP.NET server sample with an endpoint /events of type text/event-stream. This will just look and send “ticks” to a client.

app.MapGet("/events", async (HttpContext context, CancellationToken cancellationToken) =>
{
    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";

    try
    {
        for (var i = 1; i <= 20; i++)
        {
            var payload = JsonSerializer.Serialize(new
            {
                id = i,
                message = $"Server tick {i}",
                sentAtUtc = DateTimeOffset.UtcNow
            });

            await context.Response.WriteAsync($"id: {i}\n", cancellationToken);
            await context.Response.WriteAsync("event: tick\n", cancellationToken);
            await context.Response.WriteAsync($"data: {payload}\n\n", cancellationToken);
            await context.Response.Body.FlushAsync(cancellationToken);

            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
    }
    catch (OperationCanceledException)
    {
    }
});

Pretty simple. As mentioned we need to set the ContentType to text/event-stream. SSE must never be cached, hence context.Response.Headers.CacheControl = “no-cache”. The handler should not return until all responses are sent, i.e. it goes into a loop or similar and writes responses.

It’s also important to note that the SSE spec requires the following wire format

  • id: An optional event ID
  • event: An optional even name
  • datae: The actual payload
  • Ends with a blank line: Notice of the line setting the data we have two new lines, i.e. one blank line. Without this the browser will not dispatch the event
  • Flush after each event: This will force the server to push the event immediately instead of buffering, otherwise the client might receive events in chunks

C# client code

Let’s look at what’s required for a C# client to interact with our server.

The HttpClient looks like this

using var httpClient = new HttpClient
{
    BaseAddress = new Uri("http://localhost:5127")
};

Now we’ll created the request tot he /events endpoint and accept the text/event-stream i.e.

using var cancellation = new CancellationTokenSource();

using var request = new HttpRequestMessage(HttpMethod.Get, "/events");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));

using var response = await httpClient.SendAsync(
    request,
    HttpCompletionOption.ResponseHeadersRead,
    cancellation.Token);

response.EnsureSuccessStatusCode();

Okay, seo we’ve configured things and called the endpoint, now let’s look at how we might read the SSE

await using var stream = await response.Content.ReadAsStreamAsync(cancellation.Token);
using var reader = new StreamReader(stream);

while (!cancellation.IsCancellationRequested)
{
  var line = await reader.ReadLineAsync(cancellation.Token);

  if (line is null)
  {
    break;
  }

  if (line.Length == 0)
  {
    if (eventDataBuilder.Length > 0)
    {
      Console.WriteLine($"[{DateTime.Now:T}] id={eventId}, event={eventName}, data={eventDataBuilder}");
    }

    eventId = string.Empty;
    eventName = string.Empty;
    eventDataBuilder.Clear();
    continue;
  }

  if (line.StartsWith("id: ", StringComparison.Ordinal))
  {
    eventId = line[4..];
    continue;
  }

  if (line.StartsWith("event: ", StringComparison.Ordinal))
  {
    eventName = line[7..];
    continue;
  }

  if (line.StartsWith("data: ", StringComparison.Ordinal))
  {
    if (eventDataBuilder.Length > 0)
    {
      eventDataBuilder.Append('\n');
    }

    eventDataBuilder.Append(line[6..]);
}

In the example code, above, we read each line from the SSE and get each part until we’re ready to put the read data together to write to the console.

Typescript client

The following example is a React Typescript implementation of a simply UI with connect buttons etc. to connect to the events URL, using an EventSource we add a listener to and output the SSE’s as they arrive.

type TickEvent = {
  id: number
  message: string
  sentAtUtc: string
}

const maxItems = 50

function App() {
  const [events, setEvents] = useState<TickEvent[]>([])
  const [isConnected, setIsConnected] = useState(false)
  const [status, setStatus] = useState('Disconnected')
  const eventSourceRef = useRef<EventSource | null>(null)

  useEffect(() => {
    return () => {
      eventSourceRef.current?.close()
      eventSourceRef.current = null
    }
  }, [])

  const connect = () => {
    if (eventSourceRef.current) {
      return
    }

    setStatus('Connecting...')

    const eventSource = new EventSource(import.meta.env.VITE_SSE_URL ?? '/events')
    eventSourceRef.current = eventSource

    eventSource.onopen = () => {
      setIsConnected(true)
      setStatus('Connected')
    }

    eventSource.addEventListener('tick', (event) => {
      const messageEvent = event as MessageEvent<string>

      try {
        const payload = JSON.parse(messageEvent.data) as TickEvent
        setEvents((previous) => [payload, ...previous].slice(0, maxItems))
      } catch {
        setStatus('Received malformed event payload')
      }
    })

    eventSource.onerror = () => {
      setStatus('Connection lost or closed by server')
      setIsConnected(false)
      eventSource.close()
      eventSourceRef.current = null
    }
  }

  const disconnect = () => {
    eventSourceRef.current?.close()
    eventSourceRef.current = null
    setIsConnected(false)
    setStatus('Disconnected')
  }

  const clear = () => setEvents([])

  return (
    <main className="app">
      <h1>Server Sent Events Demo</h1>
      <p className="subtitle">React + TypeScript client for the ServerSentEventsSample API</p>

      <div className="controls">
        <button onClick={connect} disabled={isConnected}>Connect</button>
        <button onClick={disconnect} disabled={!isConnected}>Disconnect</button>
        <button onClick={clear} disabled={events.length === 0}>Clear</button>
      </div>

      <p className="status">
        Status: <strong>{status}</strong>
      </p>

      <ul className="event-list">
        {events.length === 0 && <li className="empty">No events yet.</li>}
        {events.map((item) => (
          <li key={`${item.id}-${item.sentAtUtc}`}>
            <div className="row">
              <span className="id">#{item.id}</span>
              <span>{item.message}</span>
            </div>
            <time dateTime={item.sentAtUtc}>
              {new Date(item.sentAtUtc).toLocaleTimeString()}
            </time>
          </li>
        ))}
      </ul>
    </main>
  )
}

Source Code

Full code for these examples is available on Github.