AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 16 — Integration and Composition: Pages Functions, Service Bindings, Webhooks

16 — Integration and Composition: Pages Functions, Service Bindings, Webhooks

August 13, 20269 min read
Download as Markdown

"Integration glue, somehow" was my composition model, and it made every integration choice feel arbitrary. The axes that fixed it: composition is either in-network (service bindings, inter-Worker communication) or over-the-wire (webhooks, external APIs), and event-driven architecture is the shape that decides which kind of glue to use where. [1][2][3] Once I saw those axes, the choices stopped feeling arbitrary.

The framing that landed is that "integration" isn't one thing on Workers — it's a family of patterns, and the right one depends on what's being composed. Two Workers in the same account talking to each other is a different problem from a Worker talking to a third-party API, which is different again from a Worker being _triggered by_ an outside event. The platform gives a distinct primitive for each.

Cloudflare network cron trigger webhook in HTTP request event-driven entry Worker A router Worker B billing Worker C notify Worker D search service bindings — in-network, no HTTP overhead Stripe API GitHub webhook Slack over-the-wire — webhooks & external APIs in-network composition is cheap; over-the-wire composition pays network cost

Pages Functions: dynamic code on a static site

The starting point for composition is often the static-site-plus-dynamic-edges pattern, and that's what Pages Functions delivers [1]. A Cloudflare Pages project deploys static assets (HTML, CSS, JS, images) to the CDN. Pages Functions (now folded into the broader Cloudflare Functions umbrella) lets me drop serverless code alongside those assets — a functions/ directory in the Pages project becomes a set of endpoints, each running as a Worker on the same edge.

The way of thinking: static content is served directly from the CDN at zero compute cost; dynamic requests (a form submission, an API call, a personalized page) hit a Pages Function that runs at the edge. Same deploy, same domain, same edge. This is the path of least resistance for "mostly static site with a few dynamic bits" — I don't stand up a separate Worker and route to it; the dynamic code lives next to the static assets and deploys with them.

Service bindings: Worker-to-Worker, in-network

When the application grows beyond one Worker, service bindings are the composition primitive that keeps it cheap [2]. A service binding lets Worker A call Worker B as if it were a function — Worker A invokes env.WORKER_B.fetch(request), the call stays inside Cloudflare's network, and the response comes back. No DNS, no TLS handshake, no public-internet round trip, no API key.

The pattern that clicked: split a monolithic Worker by bounded context. A "billing" Worker, a "notify" Worker, a "search" Worker — each independently deployable, each with its own bindings and its own scaling characteristics, all callable from a router Worker via service bindings [2]. This is microservices on Workers, but without the network penalty that usually makes microservices painful. The inter-Worker call is roughly as fast as a function call, not as slow as an HTTP request.

Inter-Worker communication

Beyond direct service bindings, there are three other ways Workers share data and coordinate [3], and the choice depends on the consistency requirement:

  • Durable Objects. For stateful coordination — multiple Workers reading and writing shared state that must stay consistent. The Durable Object is the single-actor coordinator; Workers address it by ID.
  • Shared KV namespace. For eventually-consistent shared data — a config both Workers read, a cache both can populate. Multiple Workers bind to the same KV namespace and read/write the same keys.
  • Webhooks (HTTP). When one Worker needs to trigger another across an account or boundary where a service binding doesn't apply, an HTTP call works — it's just slower and needs auth.

The rule I use: prefer service bindings for everything in-account (fastest, simplest); fall back to shared KV for read-mostly shared data; reach for Durable Objects only when the coordination genuinely requires single-actor consistency. HTTP between Workers is a last resort.

External API integration

Outward-facing composition — calling third-party services — uses the Fetch API inside a Worker [4]. The mechanics are standard fetch(), but the patterns worth internalizing:

  • Auth via secrets. API keys live in env as secrets, never in code. A Worker reads env.STRIPE_KEY and passes it as a bearer token.
  • Idempotency and retries. External calls fail; a Worker should retry with backoff and, where the API supports it, pass an idempotency key so a retried call doesn't double-charge.
  • Timeouts. A fetch() without an AbortController can hang indefinitely. For external calls, I set an explicit timeout.

The thing I had to internalize: a Worker calling a slow external API re-introduces all the latency the edge removed. If a Worker at the edge calls an API that takes 800ms, the user waits 800ms — the edge placement of the Worker didn't help. The fix is either caching the external response, making the call asynchronously (enqueuing it), or accepting that this particular feature is bound by the external dependency's speed.

Webhook handling

A webhook is the inverse — an external service calling _into_ my Worker when something happens [5]. GitHub pushes, Stripe payment events, monitoring alerts — these arrive as HTTP POSTs to a Worker endpoint, and the Worker processes them.

Two disciplines that matter:

  • Verify the signature. Every webhook provider signs its payloads with a shared secret. A Worker must verify the signature before trusting the payload, or it's vulnerable to forged webhooks. Skipping verification is a security hole.
  • Acknowledge fast, process async. The webhook sender expects a quick 2xx acknowledgement; if my processing takes long, the sender may time out and retry (causing duplicate processing). The pattern is to acknowledge immediately and push the actual work onto a Queue or Workflow, which processes it asynchronously.

Event-driven architectures

The unifying shape is event-driven [6][7]. A Worker is triggered by an event — an HTTP request, a cron firing, a webhook arriving, a queue message landing — and responds. The architecture is a graph of event sources and Workers, with state stored in the appropriate binding. The advantages are the usual ones for event-driven: loose coupling (each Worker does one thing, triggered independently), scalability (each event is handled by an ephemeral isolate), and resilience (a failed event can be retried).

The trap is the usual one too — a sprawling event graph can become hard to reason about, with chains of triggers that are difficult to trace. The discipline is to keep the graph explicit: each event source documented, each Worker's trigger clear, each piece of state's owner unambiguous.

How I use this

The composition rules I keep:

  • One Worker per bounded context, composed via service bindings. Avoid the monolithic Worker that does everything; avoid the Worker-per-endpoint over-split.
  • Pages Functions for mostly-static sites with a few dynamic endpoints. Stand up a separate Worker only when the dynamic surface is large or has different deploy cadence from the static assets.
  • Verify every webhook signature, and acknowledge before processing — push the actual work to a Queue.
  • For external API calls, set timeouts, retry with idempotency, and cache aggressively. The edge placement of the Worker doesn't help if the bottleneck is the external call.
  • Inter-Worker state: shared KV for read-mostly, Durable Objects only when coordination demands it, HTTP between Workers as a last resort.

The goal is a graph I can reason about — where each arrow is a deliberate choice of primitive, not a default.

References

[1] Cloudflare, "Functions — Cloudflare Pages," Cloudflare Docs, 2024. [Online]. Available: https://developers.cloudflare.com/pages/functions/

[2] Cloudflare, "Service Bindings — Cloudflare," Cloudflare Workers Platform, 2024. [Online]. Available: https://developers.cloudflare.com/workers/platform/service-bindings/

[3] Cloudflare, "How Workers for Platforms works — Cloudflare Docs," Cloudflare Docs. [Online]. Available: https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/

[4] Cloudflare, "APIs — Cloudflare Workers," Cloudflare Workers Configuration, 2024. [Online]. Available: https://developers.cloudflare.com/workers/configuration/integrations/apis/

[5] Cloudflare, "Configure webhooks — Cloudflare," Cloudflare Notifications Docs. [Online]. Available: https://developers.cloudflare.com/notifications/get-started/configure-webhooks/

[6] IBM, "What is event-driven architecture?," IBM Think Topics. [Online]. Available: https://www.ibm.com/think/topics/event-driven-architecture

[7] Cloudflare, "CI/CD — Cloudflare Workers," Cloudflare Docs, 2024. [Online]. Available: https://developers.cloudflare.com/workers/ci-cd/

Knowledge check · Question 1 of 5

Two Workers in the same account need to call each other frequently. What's the cheapest composition primitive?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!