Back to Thoughts & Insights

Moving from 60s to 6s: Latency Optimization Lessons from Functional Programming

About 5 min read...

The broader tech community often views functional programming (FP) as an elegant academic exercise: useful for type systems, formal reasoning, and compiler guarantees, but distant from high-throughput production systems.

That framing misses something important. FP can improve the way teams model asynchronous work, failures, and state transitions. But it is not a substitute for finding the actual source of latency.

In a distributed workflow engine, we reduced observed end-to-end completion time from roughly 60 seconds to under 6 seconds for the common successful path. The primary cause was architectural: we removed repeated polling and queue wait from the synchronous execution path. PureScript and Haskell helped us express the resulting asynchronous flow explicitly and safely; they did not, by themselves, create a 10x latency improvement.

This is the engineering story behind that change, the measurements it supports, and the tradeoffs it introduced.

What the numbers mean

The 60-second and under-6-second figures are observed end-to-end timings for the workflow’s common success path, measured from request acceptance through the final response. They are not a universal service-level objective, a benchmark of every workflow type, or a claim about every percentile under every load level.

For a production latency claim, the useful view is a before/after comparison with the same workload and scope:

  • Scope: identical successful workflow type, including validation, business-rule evaluation, external calls, state update, and response.

  • Load: compare equivalent request rate, worker availability, dependency health, and database conditions.

  • Distribution: report p50, p95, and p99, along with sample size and the observation window, rather than relying on a single elapsed time.

  • Boundaries: state whether timings include client/network time, queue time, retries, and downstream-service time.

In this case, the roughly 60-second to under-6-second result should be read as an observed common-path improvement. The main lesson is diagnostic: most of the old latency was scheduled waiting, not useful computation.

The bottleneck: polling-based workflow execution

The original system used a pull-based worker architecture. Each request moved through sequential stages: validation, business-rule evaluation, external-service interactions, state transitions, and final reconciliation.

A database-backed work queue coordinated that workflow:

  1. A worker completed a step and persisted the updated state.

  2. A later worker polled the database for pending work.

  3. After discovering the work, it executed the next stage and persisted the result.

  4. The cycle continued until the workflow completed.

This design had real benefits. It made work durable, gave operators a visible recovery point, and supported retries. It also inserted a scheduling delay between stages. With several sequential transitions, those polling intervals and queue waits accumulated.

The system was not primarily compute-bound. It was wait-bound.

The architectural change: a fast path and a durable path

We separated the responsibilities that had previously been forced through one path:

  • Fast path: execute the request directly when the workflow can complete synchronously.

  • Durable path: retain queued execution for retries, recovery, delayed work, and cases that cannot safely finish inline.

A representative fast path is:

Request → Validation → Business Rules → External Service Call → State Update → Response

Instead of persisting and waiting for a poll between every step, the request continues through that chain while the required dependencies are available. Removing those handoffs is what removed the dominant source of delay.

The queue was not eliminated because it was bad. It was moved out of the successful synchronous path because its durability and scheduling semantics were unnecessary for every transition.

Where functional programming helped

PureScript’s Aff runtime gave the direct path a useful execution model: non-blocking asynchronous effects, composable sequencing, structured error handling, and cancellation/resource-safety primitives. Similar properties are available in other ecosystems; the language was an enabler, not the performance mechanism.

FP techniques improved the implementation in three practical ways:

  • Explicit effects: database writes, remote calls, logging, and retries are visible in the program’s effectful boundary instead of being hidden in incidental control flow.

  • Typed outcomes: expected failure modes can be represented as data, making it clearer which errors respond immediately, retry, or transfer to durable processing.

  • Composable stages: validation, rule evaluation, and external interactions can be assembled and tested as small units without scattering callback or exception handling across the workflow.

These properties made the fast path easier to reason about and operate. They did not compensate for a queueing architecture that was adding avoidable wait.

Failure handling and async decoupling tradeoffs

The direct path makes a request faster by coupling more work to the request lifetime. That tradeoff needs to be deliberate.

Queued workflows decouple producers from consumers, absorb bursts, provide durable handoff points, and allow retry/recovery to proceed after the original request has ended. A synchronous fast path gives up some of that decoupling in exchange for lower latency. It can increase pressure on downstream dependencies, expose callers to longer in-flight work, and require careful timeout, cancellation, idempotency, and backpressure policies.

The design therefore needs a clear transfer rule. When the direct path encounters a retryable failure, an unavailable dependency, a deadline risk, or work that must outlive the request, it records enough durable state and hands the workflow to the durable path. That handoff must be idempotent so a timeout or ambiguous response does not duplicate an externally visible action.

Useful safeguards include:

  • per-stage deadlines and bounded retries;

  • idempotency keys for state-changing external calls;

  • circuit breaking and concurrency limits around dependencies;

  • durable audit records at defined commit points; and

  • separate metrics for direct completion, fallback, retry, and recovery outcomes.

Measure the architecture, not just the aggregate

Average latency can hide both queueing and tail failures. Instrument each transition so the system can distinguish queue wait, execution time, persistence time, and downstream-service time. Then compare p50, p95, and p99 before and after the change under matched load.

For this workflow, the key measurement was not merely that a request became faster. It was that the old path spent substantial time waiting between otherwise short stages. That evidence justified changing the execution model. The percentile view then verifies whether the fast path improves typical and tail behavior, while fallback and error metrics show whether reliability has regressed.

Conclusion

The useful conclusion is not that functional programming delivers a fixed latency multiplier. The observed reduction from roughly 60 seconds to under 6 seconds came primarily from removing polling and queue wait from the common successful path.

Functional programming contributed by making the asynchronous orchestration, failure cases, and fallback boundary easier to express and review. The durable workflow system continued to matter for the work that needs decoupling, retries, and recovery.

Find the waiting first. Then choose an architecture that removes unnecessary waiting while preserving the operational guarantees the workload actually requires.

Your Money Is Held Together by Duct Tape

Real stories from payment infrastructure, type-driven security, and production fintech engineering. I post a new blog every Sunday.

#functional-programming#programming#performance
June 20, 2026

Related Thoughts

Share Your Thoughts

Be the first to comment

Share your thoughts on this post

Join the conversation