The deciding factor is traffic shape, not workload size. A function that runs a few times an hour and spikes to hundreds of concurrent calls during a promotion does not fit a fixed container pool: a pool sized for average load either over-provisions for the spike or throttles during it. Function Compute scales per invocation, so concurrency during a spike does not require capacity planning in advance. The same function that ran twice in the last hour can run at hundreds of concurrent instances the next hour without any change to configuration.
A steady 2,000 req/s service is the opposite case. Per-invocation billing and cold-start overhead cost more there than a warm container pool on ACK, because the workload never idles long enough to benefit from scale-to-zero, and the constant cost of cold starts under sustained load outweighs the savings from not running idle containers.
A useful rule of thumb: calculate the ratio of peak concurrent invocations to average concurrent invocations over a representative two-week window. A ratio above roughly 5:1 usually favors Function Compute. A ratio close to 1:1, meaning traffic is close to constant, usually favors containers.
Cold-start latency mostly comes from what a function does on startup, not from the platform itself. A function that opens a fresh database connection, initializes an SDK client, or loads a configuration file on every invocation pays that cost repeatedly, even though Function Compute reuses execution contexts between calls whenever it can.
Initializing clients inside the handler forces a rebuild on every cold invocation:
# new client created on every invocation
import pymysql
def handler(event, context):
conn = pymysql.connect(host="polardb-endpoint", user="app", db="orders")
cursor = conn.cursor()
cursor.execute("SELECT status FROM orders WHERE id = %s", (event["order_id"],))
result = cursor.fetchone()
conn.close()
return {"status": result[0]}
Moving client initialization to module scope means it only runs once per execution context, not once per invocation:
# client initialized once per execution context, reused across invocations
import pymysql
conn = None
def get_connection():
global conn
if conn is None or not conn.open:
conn = pymysql.connect(host="polardb-endpoint", user="app", db="orders")
return conn
def handler(event, context):
cursor = get_connection().cursor()
cursor.execute("SELECT status FROM orders WHERE id = %s", (event["order_id"],))
result = cursor.fetchone()
return {"status": result[0]}
The module-level conn persists across invocations as long as Function Compute reuses the same execution context, removing the reconnect cost on warm calls. The tradeoff is that the connection can go stale if the underlying database restarts or the network path changes, which is why the get_connection check re-validates conn.open before reuse instead of assuming the cached connection is always valid.
Connection reuse only helps once a container is warm. For functions sitting directly in a synchronous request path, such as an API a mobile app calls and waits on, the first invocation after a cold period still pays the full cold-start cost. Provisioned concurrency avoids this by keeping a fixed number of instances initialized ahead of time.
Configuration using Serverless Devs (s.yaml):
edition: 1.0.0
name: order-status-service
services:
order-status:
component: fc
props:
region: cn-hangzhou
function:
name: get-order-status
runtime: python3.10
handler: index.handler
memorySize: 512
timeout: 10
provisionConfig:
target: 5
scheduledActions:
- name: business-hours-scale
startTime: "0 0 1 * * *"
endTime: "0 0 13 * * *"
target: 20
This keeps 5 instances warm at all times and scales to 20 during the defined business-hours window. On-demand instances still absorb any traffic beyond the provisioned target, so provisioned concurrency covers the predictable baseline while burst capacity remains elastic. Sizing the target value too high defeats the cost benefit of serverless compute in the first place, so it should be set from measured baseline concurrency rather than a guess.
The choice of event source affects the reliability of the whole pipeline more than the function code does. For events where message loss is unacceptable, such as payment confirmations or inventory adjustments, a RocketMQ trigger is more reliable than an HTTP webhook. An HTTP-triggered function that fails mid-execution drops the request unless the caller has its own retry logic. RocketMQ messages instead persist until acknowledged and support consumer-group semantics for retry.
Triggers:
- name: inventory-event-trigger
type: rocketmq
config:
topic: inventory-adjustments
consumerGroup: fc-inventory-consumer
region: cn-hangzhou
timeout: 60000
def handler(event, context):
import json
message = json.loads(event)
order_id = message["orderId"]
delta = message["quantityDelta"]
try:
adjust_inventory(order_id, delta)
except InventoryConflictError:
# raising here lets RocketMQ redeliver instead of dropping the event
raise
A unified commerce platform syncing stock across online and in-store channels is one example of a system that routes inventory events through RocketMQ this way, so each adjustment is processed once with retry on failure, instead of relying on a synchronous call that fails silently under load.
Retrying a failing message indefinitely can back up an entire consumer group behind one bad message, especially if the failure is caused by malformed data rather than a transient error. Configuring a dead-letter topic lets RocketMQ move a message aside after a fixed number of delivery attempts, so the rest of the queue keeps flowing:
consumerGroup: fc-inventory-consumer
maxRetries: 3
deadLetterTopic: inventory-adjustments-dlq
A separate function or scheduled job can then read from the dead-letter topic to alert on or manually reprocess the failed messages, rather than leaving them to silently retry forever inside the primary consumer group.
Functions are stateless between invocations, which matters for any event source that can redeliver a message, including RocketMQ after a timeout. Idempotency keys and rate-limit counters that need to survive across invocations go into Tair, Alibaba Cloud's in-memory database service, rather than in-process memory that disappears when the execution context is recycled:
import tair
client = tair.Tair(host="tair-endpoint", port=6379, password="***")
def handler(event, context):
event_id = event["eventId"]
# skip if this event was already processed
if client.set(f"processed:{event_id}", "1", ex=3600, nx=True) is None:
return {"status": "duplicate, skipped"}
process_event(event)
return {"status": "processed"}
nx=True sets the key only if it does not already exist, which gives a cheap distributed lock for exactly-once processing across concurrent invocations that might otherwise race on the same event ID.
Each invocation is a fresh, isolated execution that may run on a different underlying host than the previous one, so there is no long-lived process to attach a debugger to. A consistent trace ID passed through every function in a chain is what makes it possible to reconstruct a request across multiple hops after the fact:
from opentelemetry import trace
from opentelemetry.propagate import extract
tracer = trace.get_tracer("inventory-service")
def handler(event, context):
ctx = extract(event.get("headers", {}))
with tracer.start_as_current_span("adjust_inventory", context=ctx) as span:
span.set_attribute("order.id", event["orderId"])
adjust_inventory(event["orderId"], event["quantityDelta"])
Spans routed into ARMS, Alibaba Cloud's application monitoring service, let a single request be reconstructed across the API Gateway trigger, the RocketMQ hop, and any downstream functions, instead of checking several unrelated log streams to find one root cause.
Profile actual traffic for two weeks before choosing a compute model. If the peak-to-average ratio exceeds roughly 5:1 and state externalizes cleanly to Tair, Function Compute is usually lower cost and simpler to operate, since capacity planning and idle container cost both disappear. If traffic is steady and the service holds meaningful in-process state, containers on ACK remain the better fit, since the constant baseline load means Function Compute's cold-start and per-invocation costs add up without the benefit of scale-to-zero. Most production systems end up combining both, running steady-state services on ACK while routing bursty, event-driven work through Function Compute.
Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.
6 posts | 0 followers
FollowPM - C2C_Yuan - May 12, 2026
Kidd Ip - October 9, 2025
Della L. Wardhani - June 12, 2026
Alibaba Clouder - June 23, 2020
Apache Flink Community - July 28, 2025
Apache Flink Community - September 1, 2025
6 posts | 0 followers
Follow
Function Compute
Alibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.
Learn More
Container Compute Service (ACS)
A cloud computing service that provides container compute resources that comply with the container specifications of Kubernetes
Learn More
ApsaraMQ for RocketMQ
ApsaraMQ for RocketMQ is a distributed message queue service that supports reliable message-based asynchronous communication among microservices, distributed systems, and serverless applications.
Learn More
Serverless Workflow
Visualization, O&M-free orchestration, and Coordination of Stateful Application Scenarios
Learn MoreMore Posts by Ila Bandhiya