vteh.extratechsolutions

Serverless

Hand the provider your code and the event that triggers it: they run it on demand, bill per invocation, and scale to zero when idle — so you own the business logic and none of the servers under it.

Demo coming soonintermediateserverlessfaasscale-to-zerocold-startevent-drivenmanaged-compute

Every server you run is a bet that work will arrive to justify it. You provision a machine, patch its kernel, size it for some expected load, and pay for it around the clock — and then most of it sits idle most of the time, because real demand is bursty and your capacity is constant. The work you actually care about is a few hundred lines of business logic; everything else — the operating system, the runtime, the process supervisor, the scaling, the capacity math — is undifferentiated heavy lifting you maintain so that those few hundred lines have somewhere to run.

Serverless inverts the ownership. You hand the provider a unit of code and a description of the event that should trigger it; they own everything underneath. When an event arrives, they provision a sandbox, load a runtime, run your code, and bill you for the milliseconds it executed. When no events arrive, there is nothing running and nothing to pay for — the system scales to zero. When a thousand events arrive at once, they run a thousand copies. You never chose a number of servers, never patched one, and never paid for an idle one. The name is a deliberate overstatement — there are servers, you just stopped being the one who thinks about them.

The problem

Consider a workload that is genuinely event-shaped: an image is uploaded and needs a thumbnail; a webhook fires and needs processing; an HTTP request arrives and needs a response; a message lands on a topic and needs to be handled. The business logic per event is small and stateless. The load is spiky and hard to forecast — quiet for hours, then a burst. And to run it the traditional way you must stand up a long-lived service, and that service drags a tail of obligations behind it:

  • You pay for idle. The process runs 24 hours a day so it is ready for the events that arrive for two. Utilization is low, the meter never stops, and the gap between "capacity provisioned" and "work done" is pure waste.
  • You own the operational surface. OS patches, runtime upgrades, the process manager, health checks, log shipping, the box that reboots at 3 a.m. None of it is your business logic; all of it is your pager.
  • You own the scaling. A burst above your provisioned capacity is a brownout unless you have built and tuned autoscaling — which is itself a fleet to reason about, a warm-up lag to hide, and a control loop to keep from thrashing.
  • The unit of deployment is far larger than the unit of work. One handler wants to ship, but it ships as part of a whole service with its own release process, its own capacity, its own blast radius.

The recurring theme: capacity is coupled to time instead of to events. You are paying for, patching, and scaling a thing that exists continuously to serve work that occurs discretely. What you actually want is for compute to appear when an event does and cost nothing when none do — for the unit you provision and pay for to be the unit of work itself.

When to use it

Serverless earns its keep when work is event-driven, bursty, and decomposes into small stateless units:

  • The workload is a function of an event. An HTTP request, a queue message, an object-store notification, a scheduled tick, a stream record. If you can name the trigger and the handler is short-lived, it fits the model natively.
  • Load is spiky or unpredictable, and average utilization is low. Scale-to-zero turns idle from a cost into a non-event, and per-invocation billing means a 100× burst costs 100× — not a pre-provisioned fleet sized for the burst that runs empty the rest of the day. The spikier and lumpier the traffic, the stronger the case.
  • The units are small, stateless, and independently deployable. Each function does one thing, holds no durable state between calls, and ships on its own. This is the granularity the pricing and scaling models assume.
  • Operational leverage matters more than control. A small team that would rather ship handlers than run a fleet — no kernels to patch, no capacity to plan, no autoscaler to tune. You are buying back the undifferentiated heavy lifting.
  • The work fans out cleanly. Embarrassingly parallel jobs — process 10,000 files, one function invocation each — map onto per-event concurrency with no coordination code of your own.

When NOT to use it

The same properties that make serverless cheap for bursty work make it a poor fit elsewhere:

  • Sustained, high-throughput, steady load. If a tier is busy essentially all the time, you have removed the idle that scale-to-zero was saving, and now you are paying the per-invocation premium continuously. Past a break-even utilization, a reserved always-on instance is simply cheaper. Serverless bills for work done; steady load means always working.
  • Tight, predictable latency budgets sensitive to cold starts. The first request into a fresh instance pays the initialization tax, and for a p99-latency-critical path that tail is a defect. You can spend your way around it (provisioned concurrency), but at that point you are paying for warm capacity — the thing serverless was supposed to eliminate.
  • Long-running or unbounded work. Platforms cap execution duration. A job that legitimately runs for many minutes or hours does not fit; it wants a container or a batch system, possibly orchestrated by serverless but not run inside a single invocation.
  • Heavy per-instance state or large in-memory caches. Functions are ephemeral and share nothing; anything expensive to build up — a big cache, a warmed model, a pool of long-lived connections — is rebuilt on cold start and thrown away on reap. Work that depends on such state pays for it repeatedly and fits stateful compute better.
  • Chatty, low-latency coordination between many small functions. Slice an application too finely and every logical operation becomes a fan of network calls between functions — a distributed monolith with latency, retries, and partial failure smeared across a call graph that used to be an in-process method call. The boundaries must be real, or the model punishes you.
  • Deep control over the runtime. Specific kernel versions, custom system libraries, GPUs, particular CPU pinning — the managed sandbox trades that control away. If you need it, you are on the wrong side of the trade.

Architecture

A serverless function is a handler wrapped by a provider-managed lifecycle. An event source produces triggers; the platform maps each to an invocation; for each invocation it either routes to a warm instance it kept from a previous call or cold-starts a fresh one; the instance runs your handler; the result flows back to the caller or the next stage; and idle instances are eventually reaped. Concurrency is simply how many instances run at once — the platform grows it toward the event rate and shrinks it, all the way to zero, when events stop.

flowchart LR
  subgraph sources[Event sources]
    G[API Gateway<br/>HTTP request]
    Q[Queue / topic<br/>message]
    S[Object store<br/>notification]
    C[Scheduler<br/>tick]
  end
  sources --> P{Platform<br/>invoke}
  P -->|warm instance available| W[Reuse warm instance]
  P -->|none available| X[Cold start:<br/>provision + init]
  W --> H[Run handler]
  X --> H
  H --> R[Result / side effect]
  H -.idle timeout.-> Z[Reap → scale to zero]

The invocation lifecycle is where the model's costs live. A cold start pays for a sandbox, a runtime, and your initialization before the handler runs; a warm invocation skips all of that and runs the handler directly on an instance the platform happened to keep.

sequenceDiagram
  participant E as Event
  participant P as Platform
  participant I as Instance
  E->>P: trigger
  alt cold start
    P->>I: provision sandbox
    I->>I: load runtime
    I->>I: run init (imports, connections)
  end
  P->>I: invoke handler(event)
  I-->>P: result
  Note over I: kept warm briefly
  I->>I: reaped after idle timeout

The single most important structural decision is the one the hexagon already teaches: the handler is a thin adapter, and the business logic is a pure function it calls. The provider's event shape and the provider's response shape are just another boundary. Keep the domain logic ignorant of the cloud and you get code that is unit-testable without a deployment, portable across providers and even off serverless entirely, and readable as domain vocabulary rather than platform plumbing.

Code walkthrough

Start with the domain logic as a pure function — no knowledge that it will ever run in a function-as-a-service. It takes a command, returns a Result, and could run in a container, a test, or a serverless handler unchanged:

interface ThumbnailRequest {
  readonly sourceKey: string;
  readonly width: number;
}

type ThumbnailResult =
  | { ok: true; thumbnailKey: string }
  | { ok: false; reason: 'unsupported-format' | 'source-missing' };

async function createThumbnail(
  request: ThumbnailRequest,
  images: ImageStore,
): Promise<ThumbnailResult> {
  const source = await images.get(request.sourceKey);
  if (!source) return { ok: false, reason: 'source-missing' };
  if (!source.isRaster()) return { ok: false, reason: 'unsupported-format' };

  const thumbnail = source.resizedTo(request.width);
  const thumbnailKey = derivedKey(request.sourceKey, request.width);
  await images.put(thumbnailKey, thumbnail);
  return { ok: true, thumbnailKey };
}

The serverless adapter is the only part that knows about the platform. It parses the provider's event into a domain command, invokes the pure function, and maps the Result back to the provider's response contract. It contains no business rules — translation only:

export async function handler(event: ObjectCreatedEvent): Promise<HandlerResponse> {
  const request: ThumbnailRequest = {
    sourceKey: event.object.key,
    width: DEFAULT_THUMBNAIL_WIDTH,
  };

  const result = await createThumbnail(request, imageStore);

  switch (result.ok) {
    case true:
      return { status: 200, body: { thumbnailKey: result.thumbnailKey } };
    case false:
      return { status: result.reason === 'source-missing' ? 404 : 415, body: { reason: result.reason } };
  }
}

Note what lives outside the handler. Expensive initialization — the storage client, connection setup, config parsing — belongs at module scope, not inside handler. A cold start runs module initialization once; every warm invocation on that instance reuses it. Put the client construction inside the handler and you rebuild it on every call, throwing away the one optimization the warm-instance model hands you for free:

const imageStore = new ObjectStoreImageStore(readConfig());

export async function handler(event: ObjectCreatedEvent): Promise<HandlerResponse> {
  return adaptToResponse(await createThumbnail(toRequest(event), imageStore));
}

The discipline is the payoff: createThumbnail is tested against an in-memory ImageStore with no cloud in sight, the handler is a translation layer thin enough to eyeball, and moving from one FaaS provider to another — or off serverless to a container — changes the adapter and nothing else. This is exactly how this catalog's own API is structured: a Hono app of pure routes behind two composition roots, one for a Node container and one for the serverless edge, sharing every line of domain logic between them.

Cold starts

The cold start is the defining non-functional cost of serverless, and understanding it is what separates a system that uses the model well from one that fights it.

A cold start is what the platform does when no warm instance is available: allocate an isolated sandbox, load the language runtime, and run your initialization — imports, top-level code, connection setup — before your handler ever sees the event. Only then does the request run. The bill for all of that lands on the latency of that one request, and it ranges across two orders of magnitude depending on choices you control:

  • Runtime weight. A lightweight interpreted or V8-isolate runtime initializes in single-digit-to-tens of milliseconds; a heavyweight VM that must start and warm up can take hundreds of milliseconds to seconds. The runtime you pick sets the floor.
  • Package size. Everything the platform must fetch and load before init runs is on the critical path. A lean deployment artifact cold-starts faster than a fat one carrying dependencies the handler never touches. Trim the bundle.
  • Init work. Code that runs at module load — building large objects, opening connections, reading remote config — is paid on every cold start. Defer what you can to first use; keep what must be shared (a connection pool) at module scope so warm calls amortize it.

When cold-start latency is genuinely unacceptable for a path, the escape hatches all cost something. Provisioned concurrency keeps a pool of instances warm and ready — it removes the cold start by paying for idle capacity, which is the honest admission that this particular path wanted always-on compute. Keep-warm pings (invoking the function periodically to prevent reaping) are a folk remedy that works poorly under real concurrency and fights the platform's own scheduler; prefer provisioned concurrency or a different compute model. The clean framing: a cold start is the price of scale-to-zero. If you cannot tolerate the price, you may not actually want scale-to-zero on that path.

The stateless constraint

Serverless instances are ephemeral and share nothing. Any given invocation may land on a brand-new instance, an instance that served a different request a second ago, or an instance about to be reaped — and you cannot tell which. Two consequences follow, and both are load-bearing.

First, all durable state must be externalized. Anything you want to survive beyond a single invocation lives in a database, an object store, or a cache — never in instance memory, which can vanish between calls. This is precisely where Cache-Aside enters: a function reads through to a shared cache and populates it on a miss, so the expensive lookup is paid once across the whole fleet of ephemeral instances rather than rebuilt on each cold start. The externalized store is the memory that the stateless function lacks.

Second, handlers must be idempotent, because most serverless event sources deliver at least once. A queue redelivers on timeout, an event bus retries on error, a client retries on a 5xx — and the platform will happily run your handler twice for what was logically one event. If processing the same event twice corrupts state (double-charging, double-writing, double-sending), you have a latent data-integrity bug that surfaces exactly when things are already going wrong. The fix is to make the operation safe to repeat: key writes by an idempotency token, use conditional puts, dedupe on an event id. Idempotency is not an optimization here; it is a correctness requirement the delivery semantics impose on you.

Economics

The pricing model is the entire point, so it is worth being precise about where it wins and where it loses.

You pay per invocation and per unit of execution time, billed to the millisecond, and nothing at all when idle. That shape is transformative for spiky, low-average-utilization work: a function that handles a burst of a million requests and then sits quiet for a day costs almost exactly the price of a million short executions and zero for the quiet day. There is no fleet sized for the burst running empty afterward, and no capacity-planning meeting that produced a number that was wrong by 3 p.m.

But per-invocation pricing carries a premium over raw compute, and that premium compounds with utilization. Draw the two cost curves — serverless rising with the number of invocations, a reserved instance flat at its hourly rate — and they cross. Below the crossover, where load is intermittent and the reserved box would mostly idle, serverless wins decisively. Above it, where load is high and steady enough to keep an always-on instance genuinely busy, the reserved instance wins and the gap only widens. Mature systems often run both: serverless for the bursty edges, the long tail of low-traffic endpoints, and the glue between systems; provisioned compute for the hot, steady core. The architecture question is not "serverless or not" but "which workloads live on which side of the crossover" — and the honest answer changes as a workload's traffic profile changes.

Performance characteristics

The metrics above are indicative — orders of magnitude to build intuition, not measurements from a specific platform.

Idle cost is the headline, and it is genuinely zero. No invocations means no instances means no bill. This is the number that justifies the whole model for intermittent work, and nothing on a reserved-capacity system can match it — an always-on instance costs the same asleep as awake.

Cold-start latency is the tax you pay for that zero. It lands on the first request into a fresh instance and spans roughly 100 ms to 1 s depending on runtime, package size, and init weight. You cannot make it disappear without paying for warm capacity; you can only make it rare (high enough traffic that instances stay warm) or small (lean runtime, lean package, deferred init). A path that cannot absorb the tail wants provisioned concurrency — which is a quiet re-introduction of the always-on cost.

Concurrency scales with the event rate, to a limit. The platform grows instances toward demand automatically — this is auto-scaling taken to its logical extreme, per-request and provider-managed — but real accounts carry concurrency ceilings, and a function fanning out to a downstream can turn a burst into a thundering herd against a database or third-party API that has no such elasticity. The Circuit Breaker belongs exactly there, capping the pressure a suddenly-thousand-wide fleet can put on a resource that cannot scale with it.

Cost is proportional to work, which is a double-edged blade. It means you never overpay for idle — and it means a handler that blocks for two seconds on a slow downstream is billed for two seconds of doing nothing. On serverless, latency is not just a user-experience number; it is a line item. Efficiency in the handler shows up directly on the invoice.

Live demo

An interactive Serverless playground is coming soon. You will be able to drive a stream of events at a simulated function and watch the platform respond: warm instances reused with flat, fast latency; cold starts spiking the tail as fresh sandboxes provision and initialize; concurrency fanning out as the event rate climbs and collapsing back to zero as it drains — with the running bill ticking only while handlers execute. A "heavy init" toggle will let you move expensive setup between module scope and the handler and see the cold-start tax move with it, and a steady-load slider will let you push utilization past the break-even point and watch the pay-per-use curve cross the cost of an always-on instance. Check back shortly.

Related patterns

API Gateway is the front door that turns a serverless function into an HTTP service. The gateway terminates TLS, authenticates, rate-limits, and routes each request to the right function — offloading the cross-cutting concerns that would otherwise bloat every handler, and giving a fleet of small functions a single coherent surface. Serverless supplies the compute; the gateway supplies the edge. Together they are the canonical shape of a modern API: a thin routing layer in front of per-endpoint functions that scale independently and cost nothing when unused.

Pub/Sub is the event source that plays to serverless's strengths most naturally. A function subscribed to a topic is invoked once per message, scales with the publish rate, and idles to zero when the topic is quiet — the producer and the function never know each other's capacity. The pairing also demands the discipline the model requires: at-least-once delivery makes handler idempotency mandatory, and the topic decouples a spike in production from the function's ability to keep up, buffering the burst instead of dropping it. Event-driven and serverless are two names for the same architecture seen from producer and consumer sides.

Auto-scaling is the pattern serverless is, pushed to its limit — which is why they are best understood by contrast. Classic auto-scaling manages a fleet you own: you pick a target, tune a control loop, and it adds and removes instances around some minimum, reacting on a minutes-long timescale and rarely scaling to zero. Serverless hands that entire loop to the provider and takes it to the extreme: the unit is a single request, the floor is zero, and the reaction is per-invocation. You trade the control and steady-state economy of a managed fleet for the operational simplicity and idle-cost elimination of provider-managed, per-event compute. The choice between them is the choice between owning the scaling and renting it — and the crossover in their cost curves is where that choice flips.

Cache-Aside is what gives a stateless function a memory. Because instances share nothing and vanish between calls, any expensive lookup rebuilt per invocation is waste; a shared cache read through on a miss and populated for the next caller amortizes that cost across the whole ephemeral fleet. The pattern is not optional polish on serverless — it is how you claw back the locality that stateful, long-lived processes get for free and that ephemeral functions structurally cannot.

Related patterns

Discussion