tl;dr
A web service owns request-response work. A worker owns asynchronous or continuous work. Cron decides when scheduled work should begin. Queues, retries, idempotency, and failure visibility are separate contracts that must be designed explicitly.
- Keep user-blocking work in the request only when it is bounded. The caller should receive a definitive answer within the request's time and reliability budget.
- Use a worker when completion matters after the response. Slow, retryable, bursty, or continuously consumed work needs a separate owner.
- Use cron for time-based intent. A scheduler starts work approximately on schedule. It does not guarantee exactly-once completion.
- Assume duplicate execution can happen. Queue consumers and scheduled jobs should be idempotent or deduplicated at the consequence boundary.
- Make failure queryable. A background job that disappears from the request path still needs identity, attempts, terminal state, and evidence.
An agent often puts new work in the nearest function. In a web application, that usually means the request handler.
The application sends an email, processes a file, calls a model, updates several records, and returns when everything finishes. This works locally because the input is small, the network is healthy, and one developer is waiting.
Production asks a different question:
Who owns this work if the user disconnects, the request times out, the process restarts, or the same trigger arrives twice?
The answer determines whether the work belongs in a web service, worker, or scheduled job.
the three roles
| role | starts because | finishes when | typical examples |
|---|---|---|---|
| Web service | A request arrives | A response is returned | Page render, API read, validated write |
| Worker | A task or event is available | The task is acknowledged or reaches a terminal outcome | Email, OCR, import, webhook fan-out |
| Cron job | A scheduled time arrives | The invoked job completes | Daily cleanup, weekly report, periodic sync |
These roles can run the same application code. Their ownership and failure semantics differ.
use a web service for bounded request-response work
A web service is correct when the caller needs the result now and the work has a bounded execution path.
Good request work includes:
- validating input;
- authorizing the caller;
- reading a page of records;
- committing a small transaction;
- returning the identifier and state of accepted background work;
- rendering an interface.
The important boundary is not an arbitrary duration such as “under five seconds.” It is whether the operation can produce a definitive response inside the timeout and retry behavior of every layer between the caller and service.
Cloud Run returns a 504 when a request exceeds its configured timeout, but the container may continue processing that request. The client can reconnect and create another request, potentially against another instance. Google recommends retries and idempotent or resumable handlers for long requests.
That creates a dangerous shape: the caller sees failure while the original side effect may still complete.
use a worker when completion outlives the request
A worker is useful when work is:
- slow relative to the user response;
- retryable after transient failure;
- bursty enough to need buffering;
- continuously consumed from a queue or stream;
- independent of a particular HTTP connection;
- too resource-intensive to compete with request handling.
The web service should validate the request, record or enqueue the work, and return a stable identifier. The worker claims the task, performs it, records the result, and acknowledges completion.
The worker is not merely “code running in the background.” It needs an execution contract:
- task identity;
- payload schema and version;
- retry policy;
- attempt count;
- timeout;
- idempotency or deduplication rule;
- terminal failure handling;
- logs and result state;
- compatibility between producer and consumer versions.
Google Cloud Tasks documents at-least-once delivery and warns that duplicate execution can occur, so handlers should avoid harmful repeated side effects. Its overview makes idempotency part of reliable task handling.
That principle applies to background execution generally. Reliability does not mean a task runs exactly once. It means the system has a defined response to retries, crashes, and ambiguity.
cron is a trigger, not a worker model
Cron answers one question: when should work begin?
It does not, by itself, answer:
- whether overlapping runs are allowed;
- how failures retry;
- whether missed schedules catch up;
- which time zone owns the schedule;
- whether a duplicate run can happen;
- where results and failures are recorded;
- how long the job may run;
- what happens when a new deploy changes code during an active run.
Kubernetes documents that a CronJob creates Jobs approximately once per scheduled time. In some circumstances it may create two Jobs or none, so the job should be idempotent. The CronJob limitations are explicit about this behavior.
Cloud Run similarly distinguishes a service that listens for requests from a job that runs tasks and exits. Jobs may run once, on a schedule, or inside a workflow, with retries and timeouts configured separately. The job documentation describes that execution model.
Cron owns time-based intent. A job runtime owns execution. A queue may own buffering and delivery. Keep those concepts separate.
trace one feature through all three paths
Consider the document-review application from what infrastructure does a production application need?.
A customer uploads a document and wants a summary, plus a weekly report of reviewed documents.
1. the synchronous version
The web request:
- receives the file;
- stores it;
- runs OCR;
- calls a model;
- writes the summary;
- returns the completed result.
This is simple but couples the user's connection to two variable external operations. A timeout can leave unclear state. A client retry can repeat model cost or write duplicate records.
2. the worker version
The web request:
- stores the upload and document record;
- creates a processing task with a stable document identifier;
- returns
202 Acceptedand the document state.
The worker:
- claims the task;
- checks whether this processing version already completed;
- runs OCR and summarization;
- commits the result;
- records the attempt outcome;
- acknowledges the task.
The user polls or subscribes to document state. A retry uses the same idempotency key and cannot create a second accepted summary for the same processing version.
3. the scheduled version
Every Monday at 08:00 in the customer's configured time zone, cron starts report generation.
The scheduled job computes a report key from organization, reporting period, and report version. If the same schedule fires twice, both executions converge on the same report record. If a prior run is still active, the overlap policy decides whether to skip, wait, or replace it.
The scheduler did not make the report reliable. Stable identity, idempotency, and visible execution state did.
when a queue belongs between web and worker
A worker does not always require a separate broker.
A small application can store work in Postgres and let workers claim rows transactionally. This is attractive when creating the business record and its required follow-up must commit together.
A dedicated queue becomes useful when the workload needs some combination of:
- burst buffering;
- delivery leases or visibility timeouts;
- independent worker scaling;
- priority;
- retry delay;
- dead-letter handling;
- high message throughput;
- decoupling between producers and consumers.
“Queue” still needs a precise type. Pub/sub, task queues, and retained streams do not share the same offline, replay, ordering, and delivery behavior. The broader placement guide is in database vs cache vs queue vs object storage.
six questions that decide the role
Ask these before accepting an agent-generated execution path:
- Does the user need the completed result before the response? If yes, keep the bounded operation synchronous.
- Can the work safely run twice? If no, add idempotency or consequence-level deduplication before adding retries.
- Does the work begin because of a request, an event, or time? That determines the trigger, not necessarily the executor.
- Can the process restart midway? If yes, persist progress or make the operation restartable.
- What does terminal failure look like? A log line is not a task state.
- Can an operator find and retry one exact attempt? If not, the execution lacks identity.
common mistakes
- starting a detached thread after returning the HTTP response;
- running imports or model calls inside a request with no idempotency key;
- using cron every minute to imitate an event-driven queue;
- assuming a scheduled job runs exactly once;
- retrying a payment, email, or external mutation without deduplication;
- acknowledging a task before its durable result commits;
- creating a worker with no queue, poll loop, or shutdown semantics;
- leaving failed jobs visible only in raw logs;
- allowing preview workers to consume development or production tasks;
- deploying a producer change that old workers cannot parse.
Each mistake is an ownership gap. The code runs, but no component owns the ambiguous failure.
testing the production shape
A preview for this feature should preserve more than the web page.
The agent needs to verify:
- the upload request returns before processing completes;
- the preview worker receives only preview-owned work;
- duplicate delivery produces one durable result;
- a forced worker failure leaves a visible retryable task;
- the scheduled report uses the expected time zone and overlap rule;
- logs identify the service, worker, cron execution, source commit, and environment;
- teardown cannot remove or consume another environment's resources.
This is why shared staging is weak evidence for background execution. A shared queue rarely proves which branch produced or consumed a message. Branch-owned previews make the chain of custody inspectable.
where floo fits
floo models web services, workers, and cron as parts of one application topology. Repository configuration holds the reviewable service and schedule shape. GitHub state drives deployment. Routes expose request-serving components, while workers and jobs remain attributable to the same application and environment.
Preview environments preserve the relevant service topology and provide preview-owned supported managed state. Agents can inspect deploys, service-scoped logs, diagnostics, and environment identity through structured interfaces. Humans review consequential configuration or state transitions without becoming a gate for routine preview iteration.
The platform does not make an application idempotent. It gives the application enough isolation and runtime evidence to test whether its execution contract is true.
Take one slow request in your application and trace it using the three-path example. If it needs a web service, worker, schedule, and isolated state that an agent can inspect together, request access to floo.