• English
  • Go compress in Production: Compression Streams, Flush Boundaries, and Resource Control

    Go's compress packages provide streaming compression primitives rather than complete storage or transport policies.

    The standard library exposes several different compression layers:

    compress/flate  → DEFLATE bitstream
    compress/gzip   → gzip framing + DEFLATE
    compress/zlib   → zlib framing + DEFLATE
    compress/lzw    → LZW

    The distinction matters because compression is not only an algorithmic choice.

    A production compression pipeline must also define:

    • Format semantics: which wire or storage format is required.
    • Streaming semantics: when input becomes output.
    • Flush semantics: when compressed data becomes observable downstream.
    • Lifecycle semantics: when the stream is complete.
    • Resource policy: CPU, memory, input size, and decompressed output.
    • Concurrency policy: whether compressors are reused, pooled, or isolated.
    • Compatibility policy: whether encoded bytes themselves are stable or only the decoded representation matters.

    The core engineering model is:

    Compression transforms a byte stream; framing defines how that stream is represented; the application defines when, where, and how much compression is allowed.


    1. Compression Is a Stream Transformation

    The most important property of Go's compression APIs is that they compose with io.Reader and io.Writer.

    Input Stream
    
    
    ┌──────────────┐
    │ Compressor   │
    └──────┬───────┘
    
    
    Compressed Stream

    The compressor does not need to know whether its input comes from:

    • a file
    • an HTTP request
    • a database
    • object storage
    • a pipe
    • generated application data

    Likewise, its output can be sent to:

    • a file
    • an HTTP response
    • a network connection
    • another encoder
    • an archive writer

    This creates composable pipelines:

    database
    
    
    application encoder
    
    
    gzip.Writer
    
    
    HTTP response

    or:

    file
    
    
    gzip.Reader
    
    
    tar.Reader
    
    
    application

    Compression is therefore best understood as an I/O capability transformation, not as a byte-slice utility.

    Avoid designs that begin with:

    data, err := io.ReadAll(src)
    if err != nil {
        return err
    }
    
    // compress the complete data set

    unless the application genuinely requires the complete input in memory.

    The streaming design is:

    gw := gzip.NewWriter(dst)
    
    if _, err := io.Copy(gw, src); err != nil {
        return err
    }
    
    if err := gw.Close(); err != nil {
        return err
    }

    Memory consumption is then determined primarily by the compressor's internal state and the surrounding I/O pipeline rather than by the complete input size.


    2. Algorithm vs. Framing: flate, gzip, and zlib

    The compress directory contains several packages, but they do not represent interchangeable formats.

    A useful mental model is:

                     Compression algorithm
    
                  ┌──────────┴──────────┐
                  ▼                     ▼
              DEFLATE                    LZW
    
            ┌─────┴─────┐
            ▼           ▼
          gzip         zlib

    compress/flate

    flate exposes the raw DEFLATE stream.

    Use it when another protocol or container explicitly expects DEFLATE itself.

    fw, err := flate.NewWriter(dst, flate.DefaultCompression)
    if err != nil {
        return err
    }
    
    if _, err := io.Copy(fw, src); err != nil {
        return err
    }
    
    if err := fw.Close(); err != nil {
        return err
    }

    Do not choose flate merely because it is the underlying algorithm.

    If the protocol expects gzip, raw DEFLATE is the wrong wire format.

    compress/gzip

    gzip provides the gzip file format around DEFLATE.

    It is commonly used for:

    • .gz files
    • HTTP content encoding
    • compressed logs
    • Unix-style streaming pipelines
    gw := gzip.NewWriter(dst)
    
    if _, err := io.Copy(gw, src); err != nil {
        return err
    }
    
    if err := gw.Close(); err != nil {
        return err
    }

    compress/zlib

    zlib provides the zlib format around DEFLATE.

    It is appropriate when another protocol explicitly specifies zlib framing.

    zw := zlib.NewWriter(dst)
    
    if _, err := io.Copy(zw, src); err != nil {
        return err
    }
    
    if err := zw.Close(); err != nil {
        return err
    }

    The practical selection rule is:

    Need raw DEFLATE?      → flate
    Need gzip framing?     → gzip
    Need zlib framing?     → zlib
    Need LZW?              → lzw

    Format compatibility should be decided by the protocol or storage contract, not by a generic preference for one compression package.


    3. Compression Levels Are CPU/Size Trade-offs

    compress/flate exposes several compression strategies:

    NoCompression
    BestSpeed
    DefaultCompression
    BestCompression
    HuffmanOnly

    Compression level is a workload trade-off, not a quality ranking.

    A stronger compression setting can consume more CPU while reducing the output size only marginally.

    The relevant objective depends on the system:

                     objective
    
           ┌─────────────┼─────────────┐
           ▼             ▼             ▼
        latency        storage       bandwidth
           │             │             │
       BestSpeed      stronger      stronger
       may win        compression   compression

    For interactive HTTP responses:

    CPU cost → latency → user-visible response time

    may dominate.

    For long-term backups:

    CPU cost → storage reduction

    may be preferable.

    For a CPU-constrained service, aggressive compression can make the overall system slower even when it reduces network traffic.

    Default Compression

    For general-purpose data:

    gzip.NewWriter(dst)

    is usually a better starting point than manually selecting a level.

    Explicit levels should represent a measured application requirement:

    gw, err := gzip.NewWriterLevel(dst, flate.BestSpeed)
    if err != nil {
        return err
    }

    Do not encode an arbitrary compression level into a shared utility merely because it "compresses better."


    4. Flush Is a Visibility Boundary, Not Finalization

    One of the most important distinctions in streaming compression is:

    Flush makes buffered compressed data available downstream; Close completes the compressed stream.

    These operations are not interchangeable.

    Conceptually:

    Write
    
    
    internal compressor state
    
      ├── Flush ──► compressed bytes become observable
    
      └── Close ──► complete compressed stream

    Consider an incremental response:

    application
    
    
    gzip.Writer
    
    
    HTTP connection

    If the application writes a small amount of data and waits, the compressor may retain data internally.

    A flush establishes a visibility boundary:

    if _, err := gw.Write(chunk); err != nil {
        return err
    }
    
    if err := gw.Flush(); err != nil {
        return err
    }

    This is useful when downstream consumers must observe progress before the logical stream ends.

    But frequent flushing can reduce compression efficiency.

    more Flush calls
    
          ├── less buffering
          ├── earlier visibility
          └── potentially worse compression

    Therefore:

    Flush is a latency and visibility decision, not a correctness primitive.

    Use it when the surrounding protocol requires incremental delivery, not after every Write.


    5. Close Is Stream Finalization

    A compression writer is a stateful encoder.

    NewWriter
    
    
    Write
    
    
    Write
    
    
    Close
    
    
    complete compressed stream

    Calling Close is part of producing a complete compressed representation.

    This matters when compression is embedded inside another protocol:

    application → tar → gzip → network

    The finalization order must follow the dependency graph.

    if err := tw.Close(); err != nil {
        return fmt.Errorf("close tar writer: %w", err)
    }
    
    if err := gw.Close(); err != nil {
        return fmt.Errorf("close gzip writer: %w", err)
    }

    The general rule is:

    Finish inner producers before finalizing outer encoders.

    If:

    A → B

    then:

    A.Close()
    B.Close()

    This allows A to emit all remaining bytes into B before B produces its own final framing.

    Do Not Hide Finalization Errors

    This is dangerous:

    defer gw.Close()

    when the function also needs to report whether finalization succeeded.

    If the function returns successfully before the deferred call runs, an error from Close can be lost.

    For production code, explicit finalization is preferable when the finalization result affects operation success:

    if err := gw.Close(); err != nil {
        return fmt.Errorf("finalize gzip stream: %w", err)
    }

    If cleanup is needed on an error path, a deferred cleanup function can still be used, but its error semantics must be deliberately defined rather than silently discarded.


    6. Reader Lifecycle and End-of-Stream Semantics

    Decompression has the inverse structure:

    compressed stream
    
    
    gzip.Reader
    
    
    uncompressed stream

    For example:

    gr, err := gzip.NewReader(src)
    if err != nil {
        return fmt.Errorf("create gzip reader: %w", err)
    }
    defer gr.Close()
    
    if _, err := io.Copy(dst, gr); err != nil {
        return fmt.Errorf("decompress: %w", err)
    }
    
    return nil

    The compression reader and the underlying source have separate ownership boundaries.

    gzip.Reader.Close()
    
            └── closes decompressor state
    
    underlying io.Reader
    
            └── remains owned by caller

    A wrapper should not generally be assumed to own the lifetime of a file, network connection, or request body supplied by the caller.

    This follows Go's broader ownership model:

    A wrapper normally owns its wrapper state, not the lifetime of an object supplied by the caller.

    Partial Output Is Not Successful Completion

    A decompressor can produce substantial output before discovering corruption:

    compressed input
    
    
    decompress
    
           ├── 10 MB successfully decoded
    
           └── corruption discovered

    The presence of partial output does not establish successful processing.

    if _, err := io.Copy(dst, gr); err != nil {
        return fmt.Errorf("decompression failed: %w", err)
    }

    Persistent data should be committed only after the decompression operation has completed successfully.


    7. Bounded Decompression: Output Is the Resource Boundary

    Compression changes the relationship between input size and output size.

    100 KB compressed input
    
    
           decoder
    
    
    100 MB output

    Therefore:

    if compressedSize > maxInput {
        reject()
    }

    does not establish a safe output budget.

    The important boundary is often:

    maximum decompressed bytes

    Bounded Output

    The limit should be applied to the reader that produces decompressed bytes.

    func DecompressBounded(
        dst io.Writer,
        decompressedSrc io.Reader,
        maxOutput int64,
    ) error {
        // decompressedSrc is the reader returned by gzip.NewReader,
        // zlib.NewReader, or flate.NewReader.
        if maxOutput < 0 {
            return errors.New("negative output limit")
        }
    
        if maxOutput == math.MaxInt64 {
            if _, err := io.Copy(dst, decompressedSrc); err != nil {
                return fmt.Errorf("decompress: %w", err)
            }
            return nil
        }
    
        lr := io.LimitReader(decompressedSrc, maxOutput+1)
    
        n, err := io.Copy(dst, lr)
        if err != nil {
            return fmt.Errorf("decompress: %w", err)
        }
    
        if n > maxOutput {
            return fmt.Errorf(
                "decompressed output exceeds %d bytes",
                maxOutput,
            )
        }
    
        return nil
    }

    The pipeline is:

    untrusted compressed stream
    
    
          decompressor
    
    
        decompressedSrc
    
    
          LimitReader
    
    
          destination

    The critical detail is that LimitReader wraps the decompressed reader, not the original compressed input.

    Why the Parameter Name Matters

    This distinction is important enough to encode into the API.

    Prefer:

    decompressedSrc io.Reader

    over:

    src io.Reader

    because the function's safety property depends on where the limit is applied.

    The function expects:

    gzip.NewReader(...)
    
    
    decompressedSrc

    not:

    network connection
    
    
    decompressedSrc   // incorrect

    The +1 Sentinel

    The extra byte distinguishes:

    exactly maxOutput bytes → accepted
    maxOutput + 1 bytes     → exceeded

    But:

    maxOutput + 1

    can overflow when maxOutput == math.MaxInt64.

    The explicit boundary check above makes the helper safe for the complete int64 domain.


    8. Resource Domains: Compression Bombs, CPU, and Cancellation

    A decompressor is an amplification boundary.

                 untrusted input
    
    
                 decompression
    
              ┌────────┴────────┐
              ▼                 ▼
           CPU work          output bytes
              │                 │
              ▼                 ▼
           time budget       size budget

    A production service should consider at least:

    • maximum compressed input size
    • maximum decompressed output size
    • maximum processing time
    • maximum concurrent compression/decompression jobs

    A byte limit alone does not protect CPU.

    Context Does Not Automatically Cancel Compression

    This does not automatically make decompression cancellable:

    io.Copy(dst, gzipReader)

    A context.Context only controls work through components that actually observe cancellation.

    For network-backed operations, the underlying request or connection should provide the cancellation mechanism.

    For pure in-memory streams, a context-aware wrapper can establish an explicit cancellation boundary.

    type ContextWriter struct {
        ctx context.Context
        dst io.Writer
    }
    
    func (w *ContextWriter) Write(p []byte) (int, error) {
        if err := w.ctx.Err(); err != nil {
            return 0, err
        }
        return w.dst.Write(p)
    }

    The pipeline can then be:

    compressed input
    
    
    gzip.Reader
    
    
    ContextWriter
    
    
    destination

    This allows cancellation to be observed at output boundaries.

    A corresponding context-aware reader can check ctx.Err() before delegating each Read.

    However, this should not be described as a guarantee of immediate cancellation.

    A Read or Write call can still block inside the underlying implementation before control returns to the wrapper.

    For truly time-bounded operations, the underlying I/O primitive must itself provide a deadline or cancellation mechanism.

    The correct systems model is therefore:

    Context
    
    
    cancellation-aware I/O
    
    
    compression pipeline

    not:

    Context
    
    
    magically cancels arbitrary CPU work

    9. Dictionaries: Compression as Shared Capability

    flate and zlib support preset dictionaries.

    The model is:

    shared dictionary
    
     ┌─────┴─────┐
     ▼           ▼
    encoder     decoder

    Both sides must agree on the dictionary.

    Dictionaries can be useful when repeatedly compressing small payloads with highly repetitive structure:

    small JSON messages
    RPC payloads
    repeated configuration objects
    protocol records

    Without a dictionary:

    payload:
    {"user_id":123,"timestamp":...}

    the compressor must discover recurring patterns from the current stream.

    With a suitable dictionary:

    dictionary
    
        ├── common field names
        ├── common structural fragments
        └── representative repeated content

    the compressor starts with useful history.

    But dictionaries create a compatibility contract:

    encoder dictionary ≠ decoder dictionary
    
                 decoding fails

    Therefore dictionaries should be treated as protocol configuration rather than an invisible optimization.

    The right engineering sequence is:

    measure workload
    
    identify repeated patterns
    
    construct dictionary
    
    benchmark
    
    standardize dictionary version

    10. Reset and Compressor Reuse

    Compression writers and readers expose Reset methods that allow an existing compressor to be reused with a new destination or source.

    Conceptually:

    Writer
    
       ├── stream 1
    
    
    Reset
    
    
    stream 2

    This can reduce allocation pressure in high-throughput paths.

    A reused writer must be completely finished before it is reset.

    Write
    
    Close
    
    Reset
    
    Write
    
    Close

    Reset is not a substitute for Close.

    sync.Pool

    sync.Pool can be useful when profiling shows meaningful allocation pressure from repeatedly creating compression objects.

    var gzipPool = sync.Pool{
        New: func() any {
            return gzip.NewWriter(io.Discard)
        },
    }

    The important concern is not merely allocation.

    A pooled compressor carries state and may retain references associated with its previous destination.

    Before returning a compressor to the pool, reset it to a neutral destination:

    gw.Reset(io.Discard)
    gzipPool.Put(gw)

    This explicitly detaches the compressor from the previous io.Writer.

    It should be understood as reference hygiene, not as a guarantee that the compressor's internal buffers will be released.

    The implementation may retain internal memory for future reuse. That is normally the purpose of reuse rather than a memory leak.

    sync.Pool also does not provide a permanent object cache. The runtime may remove pooled objects at any time.

    Therefore:

    Use sync.Pool to reduce transient allocation pressure, not to establish deterministic object lifetime or memory capacity.

    Safe Pool Lifecycle

    A useful lifecycle is:

    pool
    
    
    borrow
    
    
    Reset(destination)
    
    
    use
    
    
    Close / finalize
    
    
    Reset(io.Discard)
    
    
    return

    If finalization fails, the safest policy is often to discard the compressor rather than return potentially problematic state to the pool.


    11. Compression and HTTP

    HTTP is one of the most common production environments for compression.

    The basic architecture is:

    application response
    
    
    gzip.Writer
    
    
    HTTP response

    Compression should be negotiated rather than blindly applied.

    The application needs to consider:

    • Accept-Encoding
    • response content type
    • payload size
    • already-compressed formats
    • latency requirements
    • CPU budget

    Compressing already-compressed content often provides little benefit:

    JPEG ──► gzip ──► almost same size + CPU cost
    ZIP  ──► gzip ──► almost same size + CPU cost
    MP4  ──► gzip ──► almost same size + CPU cost

    Compression should therefore be content-aware.

    Small Responses

    For very small payloads:

    compression overhead > bandwidth savings

    may be true.

    A response such as:

    {"ok":true}

    does not necessarily benefit from compression.

    For larger JSON, HTML, JavaScript, CSS, or text responses, compression is usually much more attractive.

    The correct threshold is workload-specific.


    12. Compression and Logging

    Compression is particularly useful for append-heavy logs and archival pipelines.

    application
    
    
    log encoder
    
    
    gzip.Writer
    
    
    rotating file

    A production log compressor should account for rotation boundaries.

    Do not keep one gzip stream open indefinitely if operational tooling expects independently readable files.

    Instead:

    app.log.001.gz
    app.log.002.gz
    app.log.003.gz

    Each file should be a complete gzip stream.

    This gives operational systems clear boundaries:

    file
    
      ├── independently readable
      ├── independently transferable
      └── independently deletable

    A stream that never reaches Close is not a completed gzip artifact.


    13. Compression and Archives

    Compression and archiving are complementary layers.

                     archive structure
    
    
                         tar
    
    
                        gzip

    This produces:

    .tar.gz

    The layers have different responsibilities:

    tar
     └── multiple named entries
    
    gzip
     └── compress one byte stream

    The inverse pipeline is:

    gzip.Reader
    
    
    tar.Reader
    
    
    archive entries

    The order matters.

    The general rule is:

    Build the pipeline according to representation layers, then unwind it in reverse order.

    This is the same lifecycle principle used by the archive package.


    14. Compression Output Is Not Canonical Data

    Compression should normally be treated as a representation rather than application data.

    Two valid compressed streams can represent identical uncompressed data while differing byte-for-byte.

    same input
    
       ├── compressor version A
       │        ↓
       │    bytes A
    
       └── compressor version B
    
             bytes B

    Both can decode to:

    same logical content

    Therefore correctness tests should generally verify:

    decode(encode(data)) == data

    rather than:

    encode(data) == expectedBytes

    unless byte-level determinism is explicitly part of the contract.

    This matters for:

    • golden tests
    • cache keys
    • artifact reproducibility
    • content hashes
    • signatures
    • build systems

    A change in the Go toolchain can alter compression output while preserving format compatibility and decoded content.

    Therefore:

    Format compatibility does not imply byte-for-byte encoding stability.


    15. Compression and Content Hashing

    A subtle architectural question is:

    What exactly is being hashed?

    These are different identities:

    H(uncompressed data)

    and:

    H(compressed representation)

    The first identifies logical content.

    The second identifies a particular encoded representation.

    Because compression output can depend on:

    • compression level
    • implementation
    • metadata
    • stream boundaries
    • toolchain version

    the compressed-byte hash can change while logical content remains identical.

    For content-addressable storage, the usual model is:

    logical content
    
    
       hash(data)
    
    
     content identity
    
    
     compressed representation

    Compression is then a storage or transport optimization rather than the source of logical identity.

    If compressed bytes themselves are signed or hashed, the encoding parameters and implementation become part of the compatibility contract.


    16. Compression and Reproducibility

    Long-lived artifacts require a stronger distinction between:

    logical content

    and:

    encoded representation

    A reproducible compressed artifact may require explicit control over:

    • compression level
    • format metadata
    • timestamps where applicable
    • implementation version
    • stream construction
    • other encoding parameters

    Do not assume that:

    gzip.NewWriter(dst)

    means identical output across all future Go toolchain versions.

    A useful architecture is:

    canonical content
    
    
    stable artifact policy
    
           ├── explicit metadata
           ├── explicit compression parameters
           └── controlled toolchain
    
    
    compressed artifact

    If reproducibility is not a requirement, the application should generally avoid imposing unnecessary byte-level constraints.


    17. Backpressure and Streaming Pipelines

    Compression introduces another important systems property: backpressure.

    Consider:

    producer
    
    
    compressor
    
    
    slow network

    If the network is slow, the compressor eventually blocks because its downstream writer blocks.

    That is normally desirable.

    producer
    
    
    compressor
    
       X  blocked
    
    slow consumer

    The pipeline naturally limits how far the producer can outrun the consumer.

    Do not automatically add unbounded buffering to "improve performance":

    producer
    
    
    unbounded queue
    
    
    compressor
    
    
    slow consumer

    Now memory becomes the backpressure reservoir.

    A bounded queue preserves a more explicit resource domain:

    producer
    
    
    bounded buffer
    
    
    compressor
    
    
    consumer

    Compression therefore participates directly in the system's flow-control architecture.


    18. CPU Concurrency Is a Resource Budget

    Compression is CPU work.

    Increasing the number of simultaneous compression operations can improve throughput until CPU contention becomes the bottleneck.

    1 worker
    
    
    CPU utilization: low
    
    8 workers
    
    
    CPU utilization: high
    
    64 workers
    
    
    scheduler contention
    cache pressure
    latency increase

    A production service should therefore treat compression concurrency as a resource budget.

    For example:

    request rate
    
    
    bounded worker pool
    
    
    compression
    
    
    network/storage

    This is especially important for:

    • API servers
    • backup workers
    • media pipelines
    • background export systems
    • multi-tenant services

    Compression should not be allowed to consume every CPU simply because requests can arrive concurrently.


    19. Compression Level Should Be a Policy, Not an API Detail

    A useful production abstraction is:

    type CompressionPolicy struct {
        Level      int
        MaxInput   int64
        MaxOutput  int64
        MaxWorkers int
    }

    The caller should express a system requirement:

    interactive response
        → low latency
    
    background backup
        → high compression ratio
    
    internal RPC
        → balanced CPU/network trade-off

    rather than:

    Compress(data, flate.BestCompression)

    everywhere.

    This keeps compression strategy at the application policy layer.

    The primitive remains:

    compress/flate
    compress/gzip
    compress/zlib

    while the policy decides:

    when
    where
    how much
    how concurrently

    Production Rules

    1. Treat compression as a stream transformation.
      Prefer io.Reader/io.Writer pipelines over whole-input buffering.

    2. Choose the format from the protocol contract.
      Use flate, gzip, zlib, or lzw according to the required representation.

    3. Separate algorithm from framing.
      DEFLATE, gzip, and zlib represent different protocol layers.

    4. Distinguish Flush from Close.
      Flush establishes a visibility boundary; Close finalizes the compressed stream.

    5. Finalize nested writers in dependency order.
      If A → B, finish A before finalizing B.

    6. Never hide important Close errors.
      Finalization can produce bytes and therefore can fail.

    7. Bound decompressed output.
      Compressed input size is not an adequate DoS boundary.

    8. Handle integer boundaries explicitly.
      Generic limit+1 helpers must account for math.MaxInt64.

    9. Make cancellation explicit.
      context.Context does not magically interrupt arbitrary compression or decompression work.

    10. Bound CPU and concurrency.
      Memory safety alone does not establish a complete resource budget.

    11. Treat dictionaries as protocol configuration.
      Encoder and decoder dictionaries must remain compatible.

    12. Reuse compressors only when measurements justify it.
      Reset and sync.Pool can reduce allocation pressure but introduce lifecycle complexity.

    13. Detach pooled compressors from previous destinations.
      Use Reset(io.Discard) before returning a compressor to sync.Pool when appropriate.

    14. Do not confuse retained buffers with memory leaks.
      Compressor implementations may retain memory intentionally for reuse.

    15. Do not treat sync.Pool as a deterministic cache.
      The runtime may discard pooled objects.

    16. Avoid compressing already-compressed data.
      Measure actual benefit rather than assuming compression always saves bandwidth.

    17. Preserve backpressure.
      Avoid unbounded buffering between producers, compressors, and consumers.

    18. Do not assume compressed bytes are canonical.
      Test decoded semantics unless byte-level stability is explicitly required.

    19. Separate logical content identity from encoded representation.
      Hash the logical content when the application cares about content identity.

    20. Make compression policy explicit.
      Compression level, output limits, worker concurrency, format, and reproducibility requirements belong at the application policy layer.


    Final Perspective

    The compress packages are small because compression itself is only one layer of a production data pipeline.

    The complete system looks more like:

                     Application Data
    
    
                     I/O Capability
                      io.Reader
                      io.Writer
    
    
                    Compression Layer
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
           flate          gzip          zlib
              │             │             │
              └─────────────┼─────────────┘
    
                     Transport / Storage
    
    
                      Resource Policy
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
           CPU limit     Size limit    Concurrency

    The compression package controls the transformation.

    The surrounding system controls the boundary.

    That distinction explains most production failures involving compression:

    • unbounded decompression
    • excessive CPU consumption
    • forgotten finalization
    • excessive flushing
    • inappropriate compression levels
    • incorrect framing
    • accidental byte-level compatibility assumptions
    • unbounded buffering
    • unsafe compressor reuse
    • missing cancellation boundaries

    The central engineering rule is:

    Compression is a representation layer, not a resource policy.

    Once that boundary is explicit, compress/flate, compress/gzip, compress/zlib, and compress/lzw become composable building blocks rather than opaque utilities.