Serverless #

Nama “serverless” adalah salah satu nama yang paling menyesatkan di dunia teknologi. Server masih ada — kamu hanya tidak mengelolanya. Yang benar-benar hilang adalah tanggung jawab untuk menyediakan, mengkonfigurasi, men-scale, dan memelihara infrastruktur server. Kamu cukup menulis fungsi, deploy, dan platform yang mengurus sisanya: scaling, availability, patching sistem operasi, dan resource allocation.

Model ini mengubah cara berpikir tentang backend: bukan lagi tentang “berapa server yang dibutuhkan” tapi tentang “fungsi apa yang perlu dieksekusi dan kapan”. Sebuah fungsi yang hanya dibutuhkan saat ada event tertentu — upload file, webhook masuk, jadwal harian — tidak perlu server yang selalu berjalan. Di serverless, kamu membayar hanya untuk eksekusi yang sebenarnya terjadi, bukan untuk kapasitas yang menunggu.

Tapi serverless bukan silver bullet. Ia memperkenalkan constraint baru — cold start, execution timeout, statelessness yang ketat, debugging yang lebih sulit — yang perlu dipahami sebelum memilihnya sebagai arsitektur.

Cara Kerja Function as a Service (FaaS) #

FaaS adalah inti dari serverless. Setiap “fungsi” adalah unit deployment yang independen — bisa ditulis dalam berbagai bahasa, di-trigger oleh berbagai event, dan di-scale secara independen.

sequenceDiagram
    participant E as Event Source
    participant P as Platform\n(AWS Lambda, GCP Functions)
    participant C as Container
    participant F as Function Code

    E->>P: Event masuk\n(HTTP request, S3 upload, SQS message)

    alt Cold Start (tidak ada container aktif)
        P->>C: Provision container baru
        C->>C: Download dan init runtime
        C->>F: Load function code
        F->>F: Inisialisasi (global scope)
        Note over C,F: Cold start: 100ms - 3 detik
    else Warm Start (container sudah ada)
        P->>C: Route ke container yang ada
        Note over C: Warm start: < 10ms
    end

    F->>F: Eksekusi handler function
    F->>E: Return response
Perbedaan model traditional server vs serverless:

  Traditional Server:
  Server selalu berjalan 24/7
  → Bayar untuk waktu server aktif, bukan hanya saat digunakan
  → Scale manual atau auto-scaling yang lambat
  → Tanggung jawab: OS update, security patch, capacity planning
  → Cocok untuk: workload yang konstan dan prediktabel

  Serverless (FaaS):
  Fungsi hanya berjalan saat ada event
  → Bayar per eksekusi (dan per GB-seconds memory yang digunakan)
  → Scale otomatis dari 0 ke ribuan concurrent dalam detik
  → Tidak ada tanggung jawab infrastruktur
  → Cocok untuk: workload yang spiky, intermittent, atau event-driven

Cold Start: Tantangan Utama Serverless #

Cold start terjadi ketika tidak ada container yang sudah “warm” untuk menangani request. Platform harus menyiapkan environment baru dari awal — ini butuh waktu yang bisa terasa oleh user jika tidak ditangani dengan baik.

Anatomi cold start:

  1. Platform provision container baru       (~50-200ms)
  2. Download dan extract deployment package (~50-500ms tergantung ukuran)
  3. Inisialisasi runtime (Node.js, Python, Java, dll)  (~50-500ms)
  4. Eksekusi kode di luar handler (global scope)       (~10-1000ms)
  5. Eksekusi handler function                          (varies)

  Total cold start:
  → Node.js/Python: biasanya 100-500ms
  → Java/C#: bisa 1-5 detik (JVM startup yang berat)
  → Go/Rust: biasanya < 100ms (binary yang compiled)

  Warm start (container yang sudah ada):
  → Langsung ke langkah 5
  → Biasanya < 10ms overhead

Strategi Mitigasi Cold Start #

# ✗ Anti-pattern: inisialisasi mahal di dalam handler
import boto3

def handler(event, context):
    # Ini dieksekusi SETIAP kali handler dipanggil
    # Bahkan untuk warm invocation yang sudah punya connection
    db_client = boto3.client('dynamodb')  # overhead setiap call
    s3_client = boto3.client('s3')
    config = load_config_from_s3()       # network call setiap kali!

    # ... proses event ...

# ✓ Benar: inisialisasi di luar handler (global scope)
# Dieksekusi SEKALI saat cold start, di-reuse di warm invocation
import boto3
import json

# Global scope — dieksekusi saat container diinisialisasi
db_client = boto3.client('dynamodb')
s3_client = boto3.client('s3')
config = None  # lazy load

def get_config():
    global config
    if config is None:
        response = s3_client.get_object(Bucket='config', Key='app.json')
        config = json.loads(response['Body'].read())
    return config

def handler(event, context):
    # Handler dipanggil setiap invocation
    # Tapi db_client, s3_client sudah ada dari global scope
    cfg = get_config()  # hanya load pertama kali per container

    # ... proses event menggunakan client yang sudah ada ...
    return process_event(event, cfg, db_client)
# Teknik mitigasi cold start lainnya:

# 1. Kurangi ukuran deployment package
# Semakin kecil package → download lebih cepat → cold start lebih pendek
# requirements.txt: hanya include yang benar-benar dibutuhkan
# Gunakan Lambda Layers untuk dependency yang jarang berubah

# 2. Provisioned Concurrency (AWS Lambda)
# Pre-warm N container sebelum ada request
# Bayar extra tapi cold start dihilangkan untuk traffic yang bisa diprediksi
# aws lambda put-provisioned-concurrency-config \
#     --function-name my-function \
#     --qualifier LIVE \
#     --provisioned-concurrent-executions 5

# 3. Scheduled ping untuk keep-alive (workaround murah)
# CloudWatch Event setiap 5 menit → invoke Lambda
# → Container tetap warm
# Tapi ini tidak scale untuk fungsi yang butuh banyak concurrent instance

# 4. Pilih runtime dengan cold start lebih cepat
# Cold start ranking (dari tercepat):
# Go/Rust > Node.js ≈ Python > C# > Java
# Untuk fungsi yang latency-sensitive, hindari Java tanpa GraalVM native image

Stateless by Design #

Serverless memaksa statelessness yang ketat — tidak ada jaminan bahwa invocation berikutnya akan menggunakan container yang sama. Setiap invocation harus memperlakukan state lokal sebagai sementara.

# ✗ Anti-pattern: menyimpan state di memory container

# State ini tidak konsisten antar invocation!
request_counter = 0  # container A: 5, container B: 3, container C: 0

def handler(event, context):
    global request_counter
    request_counter += 1
    # request_counter berbeda di setiap container
    # Tidak bisa diandalkan untuk apapun

# ✓ Benar: semua state di external storage
import boto3
import redis

# Untuk counter yang akurat
dynamodb = boto3.client('dynamodb')

def increment_request_counter(function_name: str) -> int:
    response = dynamodb.update_item(
        TableName='counters',
        Key={'name': {'S': function_name}},
        UpdateExpression='ADD #count :one',
        ExpressionAttributeNames={'#count': 'count'},
        ExpressionAttributeValues={':one': {'N': '1'}},
        ReturnValues='UPDATED_NEW'
    )
    return int(response['Attributes']['count']['N'])

# Untuk session/cache
redis_client = redis.Redis(
    host=os.environ['REDIS_HOST'],
    port=6379,
    decode_responses=True
)

def handler(event, context):
    # State selalu dari external storage
    counter = increment_request_counter('my-function')
    session = redis_client.get(f"session:{event['session_id']}")
    # ... proses ...
Di mana menyimpan state di serverless:

  Temporary (dalam satu invocation saja):
  → Local variable di function
  → /tmp filesystem (max 512MB di AWS Lambda, tapi hilang saat container recycle)

  Persistent state:
  → Database (DynamoDB, RDS via connection pooling)
  → Cache (ElastiCache/Redis via koneksi yang di-reuse di global scope)
  → Object storage (S3 untuk file)
  → Message queue (SQS, SNS untuk komunikasi async)

  State yang harus dihindari:
  → File lokal yang diharapkan persist antar invocation
  → In-memory cache yang diharapkan konsisten antar container
  → Connection yang tidak di-cleanup (connection leak di database)

Event-Driven Architecture di Serverless #

Serverless bersinar paling terang dalam arsitektur event-driven — fungsi yang dipicu oleh event dari berbagai sumber.

# Contoh Lambda function yang menangani berbagai trigger

# 1. HTTP request via API Gateway
def api_handler(event, context):
    """Handler untuk HTTP request."""
    path = event['path']
    method = event['httpMethod']
    body = json.loads(event.get('body', '{}'))

    if method == 'POST' and path == '/orders':
        return create_order(body)

    return {
        'statusCode': 404,
        'body': json.dumps({'error': 'Not found'})
    }

# 2. S3 event — ketika file di-upload
def s3_event_handler(event, context):
    """Proses file yang baru di-upload."""
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']

        # Process file: resize gambar, parse CSV, scan virus, dll
        process_uploaded_file(bucket, key)

# 3. SQS queue — proses pesan dari antrian
def sqs_handler(event, context):
    """Proses pesan dari SQS queue."""
    failed_messages = []

    for record in event['Records']:
        message_id = record['messageId']
        body = json.loads(record['body'])

        try:
            process_message(body)
        except Exception as e:
            # Tandai sebagai gagal — akan kembali ke queue atau ke DLQ
            failed_messages.append({'itemIdentifier': message_id})
            logger.error(f"Failed to process message {message_id}: {e}")

    # Return batch item failures — hanya yang gagal yang di-retry
    return {'batchItemFailures': failed_messages}

# 4. CloudWatch Events (scheduled)
def scheduled_handler(event, context):
    """Dijalankan setiap hari jam 08:00."""
    send_daily_report()
    cleanup_expired_sessions()
    return {'status': 'completed'}

# 5. DynamoDB Stream — react terhadap perubahan database
def dynamodb_stream_handler(event, context):
    """Trigger ketika ada record baru atau berubah di DynamoDB."""
    for record in event['Records']:
        if record['eventName'] == 'INSERT':
            new_item = record['dynamodb']['NewImage']
            user_id = new_item['user_id']['S']
            send_welcome_email(user_id)

Batasan Serverless yang Perlu Dipahami #

Serverless bukan tanpa keterbatasan. Memahami ini penting sebelum memilih serverless untuk use case tertentu.

Batasan utama AWS Lambda (angka bisa berbeda per provider):

  Execution timeout:
  → Maksimum 15 menit per invocation
  → Tidak cocok untuk long-running process (video encoding, ML training)

  Memory:
  → 128MB sampai 10GB (lebih banyak memory = lebih banyak CPU)
  → Tidak ada GPU (untuk workload ML/AI)

  Storage:
  → /tmp: 512MB (bisa dikonfigurasi sampai 10GB)
  → Bukan persistent storage

  Concurrent executions:
  → Default limit: 1000 concurrent per region (bisa dinaikkan)
  → Penting untuk dipahami untuk workload burst

  Deployment package:
  → 50MB compressed, 250MB uncompressed
  → Layer membantu tapi ada limit total juga

  Networking:
  → Cold start lebih lama jika di dalam VPC (ENI provisioning)
  → Perlu NAT Gateway untuk akses internet dari VPC

  Payload:
  → API Gateway: 10MB request/response
  → Synchronous invocation: 6MB payload
  → SQS: 256KB per message
Kapan serverless TIDAK cocok:

  ✗ Long-running workload (> 15 menit)
    → ETL yang memproses dataset besar
    → Video transcoding
    → ML model training
    → Solusi: ECS Fargate, EC2, AWS Batch

  ✗ Latency-sensitive yang tidak toleran cold start
    → Gaming backend real-time
    → HFT (High Frequency Trading)
    → Solusi: Provisioned Concurrency atau traditional server

  ✗ Heavy stateful workload
    → WebSocket connection yang persist lama
    → Stateful streaming (biasanya butuh connection persist)
    → Solusi: EC2/ECS dengan WebSocket server

  ✗ Workload yang butuh GPU
    → Deep learning inference
    → Solusi: EC2 GPU instances, SageMaker

  ✗ Very high constant traffic
    → Jika selalu 1000+ concurrent executions 24/7
    → Cost bisa lebih mahal dari EC2
    → Hitung dahulu sebelum memutuskan

Cost Model Serverless #

Cost model serverless berbeda fundamental dari server tradisional — ini bisa sangat hemat untuk beberapa use case, tapi juga bisa lebih mahal dari ekspektasi.

AWS Lambda pricing (2025):

  Compute:
  → $0.0000166667 per GB-second
  → Contoh: fungsi 256MB yang berjalan 1 detik
    = 0.25 GB × 1 detik × $0.0000166667 = $0.0000041667

  Request:
  → $0.20 per 1 juta request

  Free tier (per bulan):
  → 1 juta request gratis
  → 400.000 GB-seconds gratis

  Kalkulasi biaya:

  Skenario A — Low traffic:
  100 request/hari × 30 hari = 3.000 request/bulan
  Masih dalam free tier → GRATIS
  vs EC2 t3.micro: ~$8.5/bulan

  Skenario B — Medium traffic:
  100.000 request/hari × 30 hari = 3.000.000 request/bulan
  Fungsi 256MB, rata-rata 100ms
  Compute: 3M × 0.25GB × 0.1s = 75.000 GB-s × $0.0000166667 = $1.25
  Request: (3M - 1M) × $0.2/1M = $0.40
  Total: ~$1.65/bulan vs EC2 t3.micro $8.5/bulan → HEMAT

  Skenario C — High traffic:
  10 juta request/hari = 300 juta request/bulan
  Fungsi 512MB, rata-rata 200ms
  Compute: 300M × 0.5GB × 0.2s = 30M GB-s × $0.0000166667 = $500
  Request: 300M × $0.2/1M = $60
  Total: ~$560/bulan vs fleet EC2 yang bisa jadi lebih murah
  → Perlu kalkulasi lebih detail sebelum memutuskan

Observability di Serverless #

Debugging dan monitoring di serverless lebih menantang dari traditional server — tidak ada SSH ke server, log tersebar per fungsi, dan distributed tracing jadi lebih penting.

# Best practices untuk observability di Lambda

import json
import logging
import time
from functools import wraps
import boto3

# Structured logging — lebih mudah di-parse di CloudWatch Logs Insights
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

def log_event(event_type: str, **kwargs):
    """Log structured event untuk observability."""
    logger.info(json.dumps({
        'event_type': event_type,
        'timestamp': time.time(),
        **kwargs
    }))

# Decorator untuk tracing dan error handling
def traced_handler(func):
    @wraps(func)
    def wrapper(event, context):
        start_time = time.time()

        # Log invocation metadata
        log_event('invocation_start', {
            'function_name': context.function_name,
            'request_id': context.aws_request_id,
            'remaining_time_ms': context.get_remaining_time_in_millis(),
            'memory_limit_mb': context.memory_limit_in_mb,
        })

        try:
            result = func(event, context)

            duration_ms = (time.time() - start_time) * 1000
            log_event('invocation_success', {
                'duration_ms': round(duration_ms, 2),
                'request_id': context.aws_request_id,
            })

            return result

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            log_event('invocation_error', {
                'error_type': type(e).__name__,
                'error_message': str(e),
                'duration_ms': round(duration_ms, 2),
                'request_id': context.aws_request_id,
            })

            # Re-raise agar Lambda tahu ini adalah error
            raise

    return wrapper

@traced_handler
def handler(event, context):
    # Fungsi handler utama
    pass
# AWS X-Ray untuk distributed tracing

from aws_xray_sdk.core import xray_recorder, patch_all
from aws_xray_sdk.core import xray_recorder

# Patch semua AWS SDK call untuk auto-tracing
patch_all()

@xray_recorder.capture('process_order')
def process_order(order_data: dict):
    """Function ini akan muncul sebagai subsegment di X-Ray trace."""

    with xray_recorder.in_subsegment('validate_order') as subsegment:
        subsegment.put_annotation('order_id', order_data['id'])
        validate_order(order_data)

    with xray_recorder.in_subsegment('save_to_database') as subsegment:
        order = save_order(order_data)
        subsegment.put_metadata('saved_order', order)

    return order

Infrastructure as Code untuk Serverless #

# serverless.yml — menggunakan Serverless Framework

service: my-app

provider:
  name: aws
  runtime: python3.12
  region: ap-southeast-1
  memorySize: 256  # MB, default untuk semua fungsi
  timeout: 30      # detik, default
  environment:
    APP_ENV: ${opt:stage, 'development'}
    DATABASE_URL: ${env:DATABASE_URL}

  # IAM role — least privilege
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
          Resource: !GetAtt OrdersTable.Arn

functions:
  # HTTP API
  api:
    handler: src/api.handler
    events:
      - httpApi:
          path: /orders
          method: POST
      - httpApi:
          path: /orders/{id}
          method: GET
    memorySize: 512  # override default untuk fungsi yang butuh lebih

  # Scheduled job
  dailyReport:
    handler: src/reports.daily_handler
    timeout: 300  # 5 menit untuk proses report
    events:
      - schedule:
          rate: cron(0 8 * * ? *)  # Setiap hari jam 08:00 UTC
          enabled: true

  # SQS consumer
  orderProcessor:
    handler: src/processor.sqs_handler
    reservedConcurrency: 10  # Limit concurrency untuk melindungi downstream
    events:
      - sqs:
          arn: !GetAtt OrderQueue.Arn
          batchSize: 10
          functionResponseType: ReportBatchItemFailures

resources:
  Resources:
    OrdersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-${opt:stage}-orders
        BillingMode: PAY_PER_REQUEST  # On-demand — no capacity planning
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

    OrderQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-${opt:stage}-orders
        VisibilityTimeout: 60
        RedrivePolicy:
          deadLetterTargetArn: !GetAtt OrderDLQ.Arn
          maxReceiveCount: 3  # Retry 3 kali sebelum ke DLQ

    OrderDLQ:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-${opt:stage}-orders-dlq
        MessageRetentionPeriod: 1209600  # 14 hari

Anti-Pattern yang Harus Dihindari #

# ✗ Anti-pattern 1: inisialisasi mahal di dalam handler
def handler(event, context):
    db = connect_database()  # connection baru setiap invocation!
    config = load_from_s3()  # network call setiap invocation!

# ✓ Solusi: global scope untuk inisialisasi yang bisa di-reuse
db = connect_database()  # sekali per container
config = load_from_s3()  # sekali per container

def handler(event, context):
    return process(event, db, config)  # reuse dari global

# ✗ Anti-pattern 2: menyimpan state di /tmp dan mengandalkannya persist
def handler(event, context):
    with open('/tmp/data.json', 'w') as f:
        json.dump(data, f)
    # Invocation berikutnya mungkin container berbeda — file tidak ada!

# ✓ Solusi: gunakan S3 atau database untuk persistent state

# ✗ Anti-pattern 3: timeout yang terlalu pendek tanpa retry logic
# Default Lambda timeout: 3 detik
# Jika downstream service lambat → timeout sebelum selesai
# ✓ Solusi: set timeout yang realistic + implementasi retry di SQS

# ✗ Anti-pattern 4: tidak ada Dead Letter Queue
# Message gagal diproses → hilang begitu saja
# ✓ Solusi: selalu konfigurasi DLQ untuk SQS consumer dan async invocation

# ✗ Anti-pattern 5: concurrency tidak di-limit untuk downstream protection
# Lambda scale ke ribuan concurrent → flood database atau third-party API
# ✓ Solusi: set reservedConcurrency atau gunakan SQS untuk throttling alami

# ✗ Anti-pattern 6: fungsi yang melakukan terlalu banyak (monolithic Lambda)
# Satu fungsi handle semua: auth + business logic + data access + notification
# ✓ Solusi: single responsibility per fungsi, composisi via event/message

Checklist Serverless #

DESAIN FUNGSI:
  □ Setiap fungsi punya satu tanggung jawab yang jelas
  □ Handler function tipis — logika bisnis di module terpisah
  □ Inisialisasi mahal di global scope, bukan di dalam handler
  □ Tidak ada state yang disimpan di memory atau /tmp untuk persist antar invocation

COLD START:
  □ Deployment package sekecil mungkin
  □ Dependency yang tidak perlu tidak di-include
  □ Runtime dipilih berdasarkan kebutuhan latency (Go/Node.js untuk latency rendah)
  □ Provisioned Concurrency dikonfigurasi untuk fungsi yang latency-critical

ERROR HANDLING:
  □ Dead Letter Queue dikonfigurasi untuk async invocation dan SQS consumer
  □ Retry logic yang tepat (exponential backoff untuk transient error)
  □ Idempotent handler — eksekusi dua kali menghasilkan hasil yang sama
  □ Partial batch failure di-handle dengan benar (reportBatchItemFailures)

KEAMANAN:
  □ IAM role dengan least privilege — hanya permission yang diperlukan
  □ Secret dari AWS Secrets Manager atau Parameter Store, bukan env var hardcode
  □ Tidak ada sensitive data di log
  □ VPC jika fungsi perlu akses resource private

OBSERVABILITY:
  □ Structured logging (JSON) untuk kemudahan query di CloudWatch Logs Insights
  □ Custom metrics untuk business event penting
  □ X-Ray tracing diaktifkan untuk distributed tracing
  □ Alert dipasang untuk error rate, duration, throttling

COST:
  □ Memory size dikonfigurasi sesuai kebutuhan (lebih banyak memory = lebih mahal)
  □ Timeout dikonfigurasi sesuai kebutuhan (bukan set ke maksimum)
  □ Reserved concurrency di-set untuk mencegah runaway cost
  □ Cost estimate dibuat sebelum go-live

OPERASIONAL:
  □ Infrastructure as Code (Serverless Framework, AWS CDK, SAM)
  □ CI/CD pipeline untuk deployment
  □ Environment terpisah (dev, staging, production)
  □ Rollback bisa dilakukan dengan cepat

Ringkasan #

  • Serverless bukan tanpa server — ia tanpa manajemen server — platform menangani provisioning, scaling, dan availability. Kamu fokus pada kode dan logika bisnis.
  • Cold start adalah trade-off utama — kontainer baru butuh waktu untuk diinisialisasi. Minimalkan dengan inisialisasi di global scope, package yang kecil, dan runtime yang tepat.
  • Stateless adalah syarat, bukan pilihan — tidak ada jaminan invocation berikutnya pakai container yang sama. Semua state harus di external storage (DynamoDB, Redis, S3).
  • Event-driven adalah konteks terbaik serverless — file upload, webhook, pesan dari queue, jadwal periodik — semua cocok. Workload yang constant dan latency-critical lebih cocok dengan traditional server.
  • Inisialisasi di global scope untuk warm invocation — connection database, konfigurasi, dan client SDK yang di-inisialisasi di luar handler function di-reuse antar invocation dalam container yang sama.
  • Dead Letter Queue adalah safety net yang wajib — pesan yang gagal diproses tidak boleh hilang begitu saja. DLQ memungkinkan investigasi dan reprocessing.
  • IAM least privilege adalah wajib — setiap fungsi hanya boleh punya akses ke resource yang benar-benar dibutuhkan. Satu fungsi dikompromikan tidak boleh bisa mengakses semua resource.
  • Idempoten adalah keharusan — Lambda bisa mengeksekusi fungsi lebih dari sekali (exactly-once delivery tidak dijamin). Handler yang idempoten aman untuk di-retry.
  • Cost model berbeda dari tradisional — sangat hemat untuk low-to-medium traffic, bisa lebih mahal dari EC2 untuk traffic sangat tinggi yang konstan. Kalkulasi selalu sebelum memutuskan.
  • Observability lebih menantang — structured logging, X-Ray tracing, dan alert yang tepat adalah wajib karena tidak ada SSH dan log tersebar di banyak fungsi.

← Sebelumnya: Configuration Manager   Berikutnya: Microservices →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact