Lesson 07 / 20
Cases Where Serverless Is Not Suitable
Five job types are compared on the axes of duration, state, and cost: for each type the item that exceeds the threshold is written by name, and the cost of splitting an unfit job to make it fit is counted as steps, interim state, and failure points. The topic closes with a table showing which job fits serverless, which does not, and why.
Published Updated
In this lesson
- Measures five job types on the same three axes: single-run duration, state between requests, and monthly unit cost
- Writes the item that exceeds the threshold by name for each job type, and counts how many times more expensive or cheaper serverless is against a rented machine
- Works out the cost of splitting an unfit job as step count, extra duration, interim state size, and failure points
Contents
Everything measured across this topic shared the same assumption: the job fits the function. A request arrives, finishes inside the limits, keeps its state outside. Thresholds were counted, the package was shrunk, release was tied to a gate — all of it inside that assumption. Now the assumption itself is measured: which job fits inside these limits, which does not, and what is the bill for forcing an unfit job to fit?
The question is not “is serverless good or bad”; it is which job type can afford the constraints that come back in exchange for renting, and which cannot. The answer is not an opinion but a number read on three axes: duration, state, cost.
- FC37. The fictional regional measurement network’s five job types are measured. Both the workload numbers and the thresholds are fictional and represent a single runtime.
- FC38. The thresholds are fixed: a single run is nine hundred seconds, memory is three thousand seventy-two megabytes. A connection cannot outlive the request, so the same nine hundred seconds is the connection lifetime.
- FC39. Cost is written in fictional units; no real currency is used, and only ratios are meaningful. Serverless is priced per call and per gigabyte-second, the rented machine per hour.
- FC40. A rented machine carries forty concurrent jobs or two thousand persistent connections. Redundancy, load balancing, and operating labor do not enter the cost — meaning the rented machine side is counted short.
- FC41. When splitting, interim state is written and read at every boundary: eighteen plus twelve seconds and two hundred forty megabytes. The full limit cannot be used; the usable share is a scanned setting.
- FC42. A failure point is the number of places that can fail and be retried: step count plus the number of transitions between steps. The compensation path after partial success is not modeled.
Five Job Types, Three Axes
// jobs.mjs — five job types compared on duration, state, and cost; not a measurement, a model export const DURATION_LIMIT = 900; // fictional upper limit, seconds export const MEMORY_CAP = 3072; // fictional memory cap, MB export const CONNECTION_LIFETIME = 900; // a connection cannot outlive the request: same limit export const MONTH_S = 2592000; // a thirty-day month, seconds // Fictional units — not real currency, only ratios are meaningful. export const CALL = 0.0000002; // unit per call export const GB_S = 0.000017; // unit per GB-second export const MACHINE_HOUR = 0.096; // continuously running rented machine, unit per hour export const MACHINE_CONCURRENCY = 40; // concurrent work a machine carries export const MACHINE_CONNECTIONS = 2000; // Five job types. `month` is the monthly call count; for continuous services it is MONTH_S / s x concurrency. export const JOBS = { 'short and sparse request': { s: 0.12, month: 40000, mb: 256, concurrency: 1, connections: 0, state: 'none' }, 'nightly batch job': { s: 2700, month: 30, mb: 1024, concurrency: 1, connections: 0, state: 'temp file' }, 'long-running conversion': { s: 4200, month: 60, mb: 3072, concurrency: 1, connections: 0, state: 'in-memory interim result' }, 'persistent-connection service': { s: 1500, month: 1382400, mb: 512, concurrency: 20, connections: 800, state: 'session' }, 'high and continuous load': { s: 0.09, month: 1036800000, mb: 512, concurrency: 36, connections: 0, state: 'none' }, }; export const serverlessMonth = (i) => i.month * CALL + (i.month * i.s * i.mb) / 1024 * GB_S; export const machineMonth = (i) => Math.max(1, Math.ceil(i.concurrency / MACHINE_CONCURRENCY), Math.ceil(i.connections / MACHINE_CONNECTIONS)) * 730 * MACHINE_HOUR; // The item that exceeds a threshold: which limit multiplies first. Order matters, the first factor is written. export function exceeded(i) { const items = []; if (i.connections > 0 && i.s > CONNECTION_LIFETIME) items.push('connection lifetime'); else if (i.s > DURATION_LIMIT) items.push('duration limit'); if (i.mb > MEMORY_CAP) items.push('memory cap'); if (i.state === 'in-memory interim result' || i.state === 'session') items.push('state'); if (serverlessMonth(i) > machineMonth(i)) items.push('unit cost'); return items; } export const row = (g, ...x) => x.map((v, i) => String(v).padEnd(g[i])).join(''); export const unit = (x) => (x >= 100 ? x.toFixed(0) : x >= 1 ? x.toFixed(2) : x.toFixed(4)); // The line items of splitting to make a job fit (fictional): interim state is written and read at every boundary. export const INTERIM_WRITE = 18, INTERIM_READ = 12, INTERIM_MB = 240; export function split(s, share) { const usable = DURATION_LIMIT * share - INTERIM_WRITE - INTERIM_READ; if (usable <= 0) return null; const steps = Math.ceil(s / usable); const extra = (steps - 1) * (INTERIM_WRITE + INTERIM_READ); return { steps, extra, total: s + extra, interimMb: (steps - 1) * INTERIM_MB, failurePoints: 2 * steps - 1, pieceS: s / steps + INTERIM_WRITE + INTERIM_READ }; }
// run1.mjs — five job types are placed side by side on the axes of duration, state, and cost import { JOBS, DURATION_LIMIT, MEMORY_CAP, CONNECTION_LIFETIME, MACHINE_CONCURRENCY, serverlessMonth, machineMonth, exceeded, row, unit } from './jobs.mjs'; console.log(`thresholds: duration ${DURATION_LIMIT} s | memory ${MEMORY_CAP} MB | connection lifetime ${CONNECTION_LIFETIME} s`); console.log(`fictional units: a machine carries ${MACHINE_CONCURRENCY} concurrent jobs; the numbers below are units, not money\n`); const A = [32, 13, 16, 16, 10, 26]; console.log(row(A, 'job type', 'single run', 'limit multiple', 'monthly calls', 'memory', 'state') + 'concurrent'); for (const [name, i] of Object.entries(JOBS)) { console.log(row(A, name, `${i.s} s`, `${(i.s / DURATION_LIMIT).toFixed(2)}x`, i.month.toLocaleString('en-US'), `${i.mb} MB`, i.state) + i.concurrency); } const B = [32, 14, 16, 9, 26]; console.log('\nmonthly cost and the item that exceeds the threshold'); console.log(row(B, 'job type', 'serverless', 'rented machine', 'ratio') + 'exceeded threshold'); for (const [name, i] of Object.entries(JOBS)) { const s = serverlessMonth(i), m = machineMonth(i), k = exceeded(i); console.log(row(B, name, unit(s), unit(m), `${(s / m).toFixed(2)}x`) + (k.length ? k.join(' + ') : 'none')); }
thresholds: duration 900 s | memory 3072 MB | connection lifetime 900 s fictional units: a machine carries 40 concurrent jobs; the numbers below are units, not money job type single run limit multiple monthly calls memory state concurrent short and sparse request 0.12 s 0.00x 40,000 256 MB none 1 nightly batch job 2700 s 3.00x 30 1024 MB temp file 1 long-running conversion 4200 s 4.67x 60 3072 MB in-memory interim result 1 persistent-connection service 1500 s 1.67x 1,382,400 512 MB session 20 high and continuous load 0.09 s 0.00x 1,036,800,000 512 MB none 36 monthly cost and the item that exceeds the threshold job type serverless rented machine ratio exceeded threshold short and sparse request 0.0284 70.08 0.00x none nightly batch job 1.38 70.08 0.02x duration limit long-running conversion 12.85 70.08 0.18x duration limit + state persistent-connection service 17626 70.08 251.51x connection lifetime + state + unit cost high and continuous load 1001 70.08 14.28x unit cost
The second table draws the real distinction. Short and sparse request sits at the exact center of what serverless is for: monthly cost is 0.0284 units, one 2,400th of the rented machine’s. The job runs forty thousand times a month at one hundred twenty milliseconds; a machine left running continuously sits idle for nearly all of this job. This is where the handed-off capacity decision wins most clearly.
Nightly batch job and long-running conversion also win on the cost axis — one at 2% of the rented machine, the other at 18% — but both exceed the duration limit. For these two jobs the question is not “is it expensive” but “does it run at all”. Long-running conversion also keeps its state in memory: the intermediate result accumulated over the four-thousand-two-hundred-second job sits in a place that disappears when the run ends.
The last two rows turn things around. Persistent-connection service exceeds three items at once: connection lifetime, state, and unit cost. Its serverless equivalent is two hundred fifty-one times the rented machine’s, because each of the eight hundred connections is billed like a call running for its whole duration — including while it waits. High and continuous load, by contrast, falls only on the cost axis: fourteen times more expensive.
The Cost of Splitting to Fit
// run2.mjs — the cost of splitting an unfit job, and the break-even point under continuous load import { JOBS, DURATION_LIMIT, MONTH_S, INTERIM_WRITE, INTERIM_READ, INTERIM_MB, GB_S, CALL, MACHINE_HOUR, MACHINE_CONCURRENCY, split, serverlessMonth, machineMonth, row, unit } from './jobs.mjs'; const conv = JOBS['long-running conversion']; console.log(`"long-running conversion" ${conv.s} s, limit ${DURATION_LIMIT} s, interim state ${INTERIM_MB} MB, ` + `per-boundary write ${INTERIM_WRITE} s + read ${INTERIM_READ} s`); const P = [13, 16, 8, 16, 16, 15, 16]; console.log(row(P, 'limit share', 'piece duration', 'steps', 'extra duration', 'total duration', 'interim state', 'failure points') + 'monthly units'); for (const share of [0.4, 0.6, 0.8, 0.95]) { const r = split(conv.s, share); const cost = conv.month * r.steps * CALL + (conv.month * r.total * conv.mb) / 1024 * GB_S; console.log(row(P, `${share * 100}%`, `${Math.round(r.pieceS)} s`, r.steps, `${r.extra} s`, `${Math.round(r.total)} s`, `${r.interimMb} MB`, r.failurePoints) + unit(cost)); } console.log(` without splitting: 1 step, 0 s extra duration, 0 MB interim state, 1 failure point — but a single ` + `run does not work because it is ${(conv.s / DURATION_LIMIT).toFixed(2)} times the limit`); const night = JOBS['nightly batch job'], nightSplit = split(night.s, 0.8); console.log(` with the same 80% share, "nightly batch job" (${night.s} s): ${nightSplit.steps} steps, ${nightSplit.extra} s extra duration, ` + `${nightSplit.interimMb} MB interim state, ${nightSplit.failurePoints} failure points`); // Persistent connection: the job cannot be split, the connection breaks. Its cost is measured by the reconnect count. const conn = JOBS['persistent-connection service']; const breaks = Math.ceil(MONTH_S / DURATION_LIMIT) * conn.connections; console.log(`\n"persistent-connection service": ${conn.connections} connections, average lifetime ${conn.s} s`); console.log(` because the limit is ${DURATION_LIMIT} s, every connection breaks ${Math.ceil(MONTH_S / DURATION_LIMIT)} times a month: ` + `${breaks.toLocaleString('en-US')} reconnects; session state is read externally every time`); // Break-even under continuous load: at which req/s the rented machine gets cheaper. const load = JOBS['high and continuous load']; const Y = [12, 17, 15, 14, 10]; console.log('\nbreak-even under continuous load — req/s is scanned'); console.log(row(Y, 'req/s', 'concurrent jobs', 'serverless/mo', 'machine/mo', 'machines') + 'cheaper'); for (const r of [1, 10, 28, 100, 400]) { const i = { ...load, month: Math.round(r * MONTH_S), concurrency: Math.ceil(r * load.s) }; const s = serverlessMonth(i), m = machineMonth(i); console.log(row(Y, r, i.concurrency, unit(s), unit(m), Math.max(1, Math.ceil(i.concurrency / MACHINE_CONCURRENCY))) + (s < m ? 'serverless' : 'rented machine')); }
"long-running conversion" 4200 s, limit 900 s, interim state 240 MB, per-boundary write 18 s + read 12 s limit share piece duration steps extra duration total duration interim state failure points monthly units 40% 353 s 13 360 s 4560 s 2880 MB 25 13.95 60% 497 s 9 240 s 4440 s 1920 MB 17 13.59 80% 630 s 7 180 s 4380 s 1440 MB 13 13.40 95% 730 s 6 150 s 4350 s 1200 MB 11 13.31 without splitting: 1 step, 0 s extra duration, 0 MB interim state, 1 failure point — but a single run does not work because it is 4.67 times the limit with the same 80% share, "nightly batch job" (2700 s): 4 steps, 90 s extra duration, 720 MB interim state, 7 failure points "persistent-connection service": 800 connections, average lifetime 1500 s because the limit is 900 s, every connection breaks 2880 times a month: 2,304,000 reconnects; session state is read externally every time break-even under continuous load — req/s is scanned req/s concurrent jobs serverless/mo machine/mo machines cheaper 1 1 2.50 70.08 1 serverless 10 1 25.01 70.08 1 serverless 28 3 70.04 70.08 1 serverless 100 9 250 70.08 1 rented machine 400 36 1001 70.08 1 rented machine
The splitting scan says this: the cost of splitting is not slowing the job down. Even in the worst setting, total duration only climbs from four thousand two hundred to four thousand five hundred sixty seconds, 8.6%. The real bill is in the other three columns. When the limit share is pulled to 40%, step count climbs to thirteen, interim state to two thousand eight hundred eighty megabytes, failure points to twenty-five; at 95% these drop to six steps, one thousand two hundred megabytes, and eleven failure points — but getting closer to the limit means a piece whose duration runs slightly long can exceed it.
So splitting turns a job with a single failure point into one with eleven to twenty-five failure points, and makes every point in between dependent on writing and reading an interim state, and therefore on a persistent store. At the same 80% setting, the nightly batch job pays four steps, ninety seconds of extra duration, seven hundred twenty megabytes of interim state, and seven failure points. These are line items that come from serverless’s constraint, not from the job itself.
The persistent-connection row shows the limit of splitting. This job cannot be split: when the limit is reached, what gets cut is not computation but the connection. Eight hundred connections break two million three hundred four thousand times a month, and every time one breaks, the session state is read back from outside. The cost here is not a slowdown but a change in design.
The break-even scan ties the last axis to a single number. Up to twenty-eight requests per second serverless is cheaper; past that the rented machine is cheaper, and at four hundred requests the difference climbs to fourteen times. This number does not count redundancy or operating labor, so the real break-even point sits above twenty-eight. What the measurement says is this: serverless’s cost advantage is inversely proportional to occupancy.
Which Job Fits, Which Does Not
| Job Type | Does It Fit? | Item That Exceeds the Threshold | Cost of the Workaround |
|---|---|---|---|
| Short and sparse request | Fits | none | none — 0.03 units a month, one 2,400th of the machine’s cost |
| Nightly batch job | Fits if split | duration limit: 2,700 s, 3x the limit | 4 steps, 90 s, 720 MB interim state, 7 failure points |
| Long-running conversion | Strained | duration limit: 4,200 s, 4.67x the limit + in-memory state | 7 steps, 180 s, 1,440 MB interim state, 13 failure points |
| Persistent-connection service | Does not fit | connection lifetime 900 s < 1,500 s + session state + cost 251x | cannot be split; 2,304,000 reconnects a month |
| High and continuous load | Does not fit | unit cost: machine gets cheaper above 28 req/s, 14x at 400 | none — the constraint is cost, cannot be worked around by design |
Summary
- Short and sparse request costs 0.0284 units a month on serverless, one 2,400th of the rented machine’s; a machine left running continuously sits idle for nearly all of this job.
- Nightly batch job and long-running conversion win on cost but exceed the duration limit by three and 4.67 times; conversion also keeps its state in memory.
- Persistent-connection service exceeds three items at once, and its serverless equivalent is two hundred fifty-one times the rented machine’s; a connection is billed even while it waits.
- The cost of splitting is not duration: total duration stretches at most 8.6%. The cost is step count, interim state, and failure points — for the conversion, six to thirteen steps, one thousand two hundred to two thousand eight hundred eighty megabytes, and eleven to twenty-five failure points.
- Persistent connection cannot be split: what gets cut at the limit is not computation but the connection, meaning two million three hundred four thousand reconnects a month.
- Under continuous load the break-even point is twenty-eight requests per second; above it the rented machine is cheaper, and at four hundred requests the difference is fourteen times. Serverless’s cost advantage is inversely proportional to occupancy.
Next Step
The function’s limits were counted: its duration, memory, concurrency, package, release, rollback, and where it cannot be used. Everything measured across this topic belonged to a single function — a request went in, a response came out, and a limit sat in between. But none of the measurement network’s jobs end with a single function: reading arrives, validation fires; validation finishes, billing is generated; billing is generated, a notification goes out. This chain itself was never measured. How will functions connect to one another — will one call the other directly, or will something sit between them? The next topic measures the connection itself.
To keep your progress and take notes, Log in
My notes
Log in to take notes.