Synchronous vs Asynchronous Processing — How I Decide in Real Projects

by Arif Ikhsanudin, Backend Developer

The wrong reason most teams go async

An engineer profiles the order creation endpoint. It takes 800ms. Most of that is the email confirmation being sent synchronously via SMTP. Someone suggests making it async: push the email to a background job queue, return the response immediately, deliver the email out-of-band. Response time drops to 50ms. Problem solved, case closed.

Except: the caller — a React frontend — was already handling a 800ms wait with a loading spinner. The user experience does not change. What changed is the architecture, which now has a Redis dependency, a Sidekiq worker fleet, retry logic, dead letter queues, and monitoring for worker lag. You added complexity to solve a problem that did not exist. This is the most common way teams introduce unnecessary async processing.

The actual question: what are you promising the caller?

Synchronous processing makes a contract: "When I return HTTP 200, the work is done." The caller can rely on that. If you return 200 and the email failed, that is a bug in your system, not a contract ambiguity.

Asynchronous processing makes a different contract: "I have accepted your request. The work will happen eventually." The caller cannot rely on the work being done when the response arrives. If the caller needs the result immediately — to render a UI, to make a downstream decision, to pass data to another system — async processing requires additional machinery to communicate completion: webhooks, polling, WebSockets, or SSE (Server-Sent Events).

That machinery is not free. If your caller needs synchronous semantics but you chose async processing, you have not simplified the system — you have moved the complexity from the server to the protocol between client and server.

When synchronous is clearly right

Synchronous processing is right whenever: the operation completes in under 2-3 seconds on P99, the caller needs the result to proceed, and the operation failure should surface immediately as an error response.

Database reads and writes are the obvious case. Creating a user, updating a record, running a search query — these should be synchronous. The caller needs confirmation that the record exists before rendering the UI. Making record creation async to avoid a 50ms database write is almost never the right optimization.

Form validation, payment authorization, permission checks — all synchronous. The user is waiting. The decision needs to happen before the next step.

# Synchronous — right for anything the user is waiting on
class OrdersController < ApplicationController
  def create
    order = Order.new(order_params)

    if order.save
      # Payment authorization is synchronous — the user needs the result now
      result = PaymentService.authorize(order, payment_params)

      if result.success?
        order.update!(status: :confirmed, payment_id: result.charge_id)
        render json: OrderSerializer.new(order), status: :created
      else
        order.update!(status: :payment_failed)
        render json: { error: result.error_message }, status: :payment_required
      end
    else
      render json: { errors: order.errors }, status: :unprocessable_entity
    end
  end
end

When async is clearly right

Move work async when all three of these are true: the caller does not need the result to proceed, the work could take longer than your HTTP timeout budget, or the work is a side effect that should not block the primary transaction.

Email and notification delivery: async. The user does not wait for the email to send. If it fails, you retry. The core operation — order created, user registered — is separate from the notification.

Third-party webhook dispatching: async. External APIs are slow and fail. A synchronous call to a webhook endpoint inside your request cycle means your response time is coupled to a third party's SLA. That is not acceptable.

Report generation, data exports, video processing: async. These are long-running operations that will exceed HTTP timeouts (typically 30-60 seconds). Use async processing with a job status endpoint the client can poll, or a webhook callback.

# Async — right for side effects and long-running work
class OrdersController < ApplicationController
  def create
    order = Order.create!(order_params.merge(status: :confirmed))

    # Side effects — enqueue, don't block
    OrderConfirmationMailer.delay.send_confirmation(order.id)
    InventoryService.delay.reserve_items(order.id)
    AnalyticsJob.perform_later(event: 'order_created', order_id: order.id)

    render json: OrderSerializer.new(order), status: :created
  end
end

The failure mode you must design for

Async processing introduces a failure class that synchronous processing does not have: the operation was accepted but never completed. Your queue worker crashed. Redis ran out of memory. The downstream service was down during all retry attempts. The job ended up in the dead letter queue and nobody noticed for three days.

With synchronous processing, a failure is immediate and visible — the HTTP response is an error. With async processing, you need explicit monitoring for job queue lag, dead letter queue depth, and worker health. This is not optional; it is the operational cost of deferral.

Tools like Sidekiq Pro (job unique locking, rate limiting, reliable scheduling) or SQS with FIFO queues and visibility timeout configuration give you the primitives to handle these failure cases. But you have to design for them. The common mistake is treating async processing as "fire and forget" when it is actually "fire and eventually confirm."

The practical rule

Draw the line at user-waiting. If a human is waiting for the response to proceed, process synchronously and optimize the operation if it is slow. If the response is acknowledging receipt of work that will happen later, process asynchronously and invest in the operational tooling to monitor and recover that deferred work.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

The Hidden Cost of Over-Managed Developer Teams

At first, more control feels safer—more meetings, more approvals, more tracking. But slowly, productivity drops, and no one can quite explain why.

Read more

Message Queues: The Part of System Design Most Backends Skip Too Long

Asynchronous messaging solves a class of reliability and decoupling problems that synchronous HTTP calls cannot. Most teams discover this after their first major production incident involving a slow downstream dependency.

Read more

Retry Logic Sounds Simple Until It Makes Things Worse

Naive retry implementations amplify load on struggling services, create duplicate side effects, and produce thundering herd problems at recovery time. Getting retries right requires exponential backoff, jitter, idempotency, and budget limits.

Read more

Berlin Has a Backend Developer Shortage. Remote Contractors Fill the Gap

You've been hiring for three months. The role is still open. The gap in your backend isn't waiting for you to find the perfect candidate.

Read more