Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close